Module exchangelib.version

Classes

class Build (major_version, minor_version, major_build=0, minor_build=0)
Expand source code
class Build:
    """Holds methods for working with build numbers."""

    __slots__ = "major_version", "minor_version", "major_build", "minor_build"

    def __init__(self, major_version, minor_version, major_build=0, minor_build=0):
        if not isinstance(major_version, int):
            raise InvalidTypeError("major_version", major_version, int)
        if not isinstance(minor_version, int):
            raise InvalidTypeError("minor_version", minor_version, int)
        if not isinstance(major_build, int):
            raise InvalidTypeError("major_build", major_build, int)
        if not isinstance(minor_build, int):
            raise InvalidTypeError("minor_build", minor_build, int)
        self.major_version = major_version
        self.minor_version = minor_version
        self.major_build = major_build
        self.minor_build = minor_build
        if major_version < 8:
            raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})")

    @classmethod
    def from_xml(cls, elem):
        xml_elems_map = {
            "major_version": "MajorVersion",
            "minor_version": "MinorVersion",
            "major_build": "MajorBuildNumber",
            "minor_build": "MinorBuildNumber",
        }
        kwargs = {}
        for k, xml_elem in xml_elems_map.items():
            v = elem.get(xml_elem)
            if v is None:
                v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}")
                if v is None:
                    raise ValueError()
            kwargs[k] = int(v)  # Also raises ValueError
        return cls(**kwargs)

    def api_version(self):
        for build, api_version, _ in VERSIONS:
            if self.major_version != build.major_version or self.minor_version != build.minor_version:
                continue
            if self >= build:
                return api_version
        raise ValueError(f"API version for build {self} is unknown")

    def __cmp__(self, other):
        # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators
        c = (self.major_version > other.major_version) - (self.major_version < other.major_version)
        if c != 0:
            return c
        c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version)
        if c != 0:
            return c
        c = (self.major_build > other.major_build) - (self.major_build < other.major_build)
        if c != 0:
            return c
        return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build)

    def __eq__(self, other):
        return self.__cmp__(other) == 0

    def __hash__(self):
        return hash(repr(self))

    def __ne__(self, other):
        return self.__cmp__(other) != 0

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __le__(self, other):
        return self.__cmp__(other) <= 0

    def __gt__(self, other):
        return self.__cmp__(other) > 0

    def __ge__(self, other):
        return self.__cmp__(other) >= 0

    def __str__(self):
        return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}"

    def __repr__(self):
        return self.__class__.__name__ + repr(
            (self.major_version, self.minor_version, self.major_build, self.minor_build)
        )

Holds methods for working with build numbers.

Static methods

def from_xml(elem)

Instance variables

var major_build
Expand source code
class Build:
    """Holds methods for working with build numbers."""

    __slots__ = "major_version", "minor_version", "major_build", "minor_build"

    def __init__(self, major_version, minor_version, major_build=0, minor_build=0):
        if not isinstance(major_version, int):
            raise InvalidTypeError("major_version", major_version, int)
        if not isinstance(minor_version, int):
            raise InvalidTypeError("minor_version", minor_version, int)
        if not isinstance(major_build, int):
            raise InvalidTypeError("major_build", major_build, int)
        if not isinstance(minor_build, int):
            raise InvalidTypeError("minor_build", minor_build, int)
        self.major_version = major_version
        self.minor_version = minor_version
        self.major_build = major_build
        self.minor_build = minor_build
        if major_version < 8:
            raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})")

    @classmethod
    def from_xml(cls, elem):
        xml_elems_map = {
            "major_version": "MajorVersion",
            "minor_version": "MinorVersion",
            "major_build": "MajorBuildNumber",
            "minor_build": "MinorBuildNumber",
        }
        kwargs = {}
        for k, xml_elem in xml_elems_map.items():
            v = elem.get(xml_elem)
            if v is None:
                v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}")
                if v is None:
                    raise ValueError()
            kwargs[k] = int(v)  # Also raises ValueError
        return cls(**kwargs)

    def api_version(self):
        for build, api_version, _ in VERSIONS:
            if self.major_version != build.major_version or self.minor_version != build.minor_version:
                continue
            if self >= build:
                return api_version
        raise ValueError(f"API version for build {self} is unknown")

    def __cmp__(self, other):
        # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators
        c = (self.major_version > other.major_version) - (self.major_version < other.major_version)
        if c != 0:
            return c
        c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version)
        if c != 0:
            return c
        c = (self.major_build > other.major_build) - (self.major_build < other.major_build)
        if c != 0:
            return c
        return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build)

    def __eq__(self, other):
        return self.__cmp__(other) == 0

    def __hash__(self):
        return hash(repr(self))

    def __ne__(self, other):
        return self.__cmp__(other) != 0

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __le__(self, other):
        return self.__cmp__(other) <= 0

    def __gt__(self, other):
        return self.__cmp__(other) > 0

    def __ge__(self, other):
        return self.__cmp__(other) >= 0

    def __str__(self):
        return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}"

    def __repr__(self):
        return self.__class__.__name__ + repr(
            (self.major_version, self.minor_version, self.major_build, self.minor_build)
        )
var major_version
Expand source code
class Build:
    """Holds methods for working with build numbers."""

    __slots__ = "major_version", "minor_version", "major_build", "minor_build"

    def __init__(self, major_version, minor_version, major_build=0, minor_build=0):
        if not isinstance(major_version, int):
            raise InvalidTypeError("major_version", major_version, int)
        if not isinstance(minor_version, int):
            raise InvalidTypeError("minor_version", minor_version, int)
        if not isinstance(major_build, int):
            raise InvalidTypeError("major_build", major_build, int)
        if not isinstance(minor_build, int):
            raise InvalidTypeError("minor_build", minor_build, int)
        self.major_version = major_version
        self.minor_version = minor_version
        self.major_build = major_build
        self.minor_build = minor_build
        if major_version < 8:
            raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})")

    @classmethod
    def from_xml(cls, elem):
        xml_elems_map = {
            "major_version": "MajorVersion",
            "minor_version": "MinorVersion",
            "major_build": "MajorBuildNumber",
            "minor_build": "MinorBuildNumber",
        }
        kwargs = {}
        for k, xml_elem in xml_elems_map.items():
            v = elem.get(xml_elem)
            if v is None:
                v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}")
                if v is None:
                    raise ValueError()
            kwargs[k] = int(v)  # Also raises ValueError
        return cls(**kwargs)

    def api_version(self):
        for build, api_version, _ in VERSIONS:
            if self.major_version != build.major_version or self.minor_version != build.minor_version:
                continue
            if self >= build:
                return api_version
        raise ValueError(f"API version for build {self} is unknown")

    def __cmp__(self, other):
        # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators
        c = (self.major_version > other.major_version) - (self.major_version < other.major_version)
        if c != 0:
            return c
        c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version)
        if c != 0:
            return c
        c = (self.major_build > other.major_build) - (self.major_build < other.major_build)
        if c != 0:
            return c
        return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build)

    def __eq__(self, other):
        return self.__cmp__(other) == 0

    def __hash__(self):
        return hash(repr(self))

    def __ne__(self, other):
        return self.__cmp__(other) != 0

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __le__(self, other):
        return self.__cmp__(other) <= 0

    def __gt__(self, other):
        return self.__cmp__(other) > 0

    def __ge__(self, other):
        return self.__cmp__(other) >= 0

    def __str__(self):
        return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}"

    def __repr__(self):
        return self.__class__.__name__ + repr(
            (self.major_version, self.minor_version, self.major_build, self.minor_build)
        )
var minor_build
Expand source code
class Build:
    """Holds methods for working with build numbers."""

    __slots__ = "major_version", "minor_version", "major_build", "minor_build"

    def __init__(self, major_version, minor_version, major_build=0, minor_build=0):
        if not isinstance(major_version, int):
            raise InvalidTypeError("major_version", major_version, int)
        if not isinstance(minor_version, int):
            raise InvalidTypeError("minor_version", minor_version, int)
        if not isinstance(major_build, int):
            raise InvalidTypeError("major_build", major_build, int)
        if not isinstance(minor_build, int):
            raise InvalidTypeError("minor_build", minor_build, int)
        self.major_version = major_version
        self.minor_version = minor_version
        self.major_build = major_build
        self.minor_build = minor_build
        if major_version < 8:
            raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})")

    @classmethod
    def from_xml(cls, elem):
        xml_elems_map = {
            "major_version": "MajorVersion",
            "minor_version": "MinorVersion",
            "major_build": "MajorBuildNumber",
            "minor_build": "MinorBuildNumber",
        }
        kwargs = {}
        for k, xml_elem in xml_elems_map.items():
            v = elem.get(xml_elem)
            if v is None:
                v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}")
                if v is None:
                    raise ValueError()
            kwargs[k] = int(v)  # Also raises ValueError
        return cls(**kwargs)

    def api_version(self):
        for build, api_version, _ in VERSIONS:
            if self.major_version != build.major_version or self.minor_version != build.minor_version:
                continue
            if self >= build:
                return api_version
        raise ValueError(f"API version for build {self} is unknown")

    def __cmp__(self, other):
        # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators
        c = (self.major_version > other.major_version) - (self.major_version < other.major_version)
        if c != 0:
            return c
        c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version)
        if c != 0:
            return c
        c = (self.major_build > other.major_build) - (self.major_build < other.major_build)
        if c != 0:
            return c
        return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build)

    def __eq__(self, other):
        return self.__cmp__(other) == 0

    def __hash__(self):
        return hash(repr(self))

    def __ne__(self, other):
        return self.__cmp__(other) != 0

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __le__(self, other):
        return self.__cmp__(other) <= 0

    def __gt__(self, other):
        return self.__cmp__(other) > 0

    def __ge__(self, other):
        return self.__cmp__(other) >= 0

    def __str__(self):
        return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}"

    def __repr__(self):
        return self.__class__.__name__ + repr(
            (self.major_version, self.minor_version, self.major_build, self.minor_build)
        )
var minor_version
Expand source code
class Build:
    """Holds methods for working with build numbers."""

    __slots__ = "major_version", "minor_version", "major_build", "minor_build"

    def __init__(self, major_version, minor_version, major_build=0, minor_build=0):
        if not isinstance(major_version, int):
            raise InvalidTypeError("major_version", major_version, int)
        if not isinstance(minor_version, int):
            raise InvalidTypeError("minor_version", minor_version, int)
        if not isinstance(major_build, int):
            raise InvalidTypeError("major_build", major_build, int)
        if not isinstance(minor_build, int):
            raise InvalidTypeError("minor_build", minor_build, int)
        self.major_version = major_version
        self.minor_version = minor_version
        self.major_build = major_build
        self.minor_build = minor_build
        if major_version < 8:
            raise ValueError(f"Exchange major versions below 8 don't support EWS ({self})")

    @classmethod
    def from_xml(cls, elem):
        xml_elems_map = {
            "major_version": "MajorVersion",
            "minor_version": "MinorVersion",
            "major_build": "MajorBuildNumber",
            "minor_build": "MinorBuildNumber",
        }
        kwargs = {}
        for k, xml_elem in xml_elems_map.items():
            v = elem.get(xml_elem)
            if v is None:
                v = get_xml_attr(elem, f"{{{ANS}}}{xml_elem}")
                if v is None:
                    raise ValueError()
            kwargs[k] = int(v)  # Also raises ValueError
        return cls(**kwargs)

    def api_version(self):
        for build, api_version, _ in VERSIONS:
            if self.major_version != build.major_version or self.minor_version != build.minor_version:
                continue
            if self >= build:
                return api_version
        raise ValueError(f"API version for build {self} is unknown")

    def __cmp__(self, other):
        # __cmp__ is not a magic method in Python3. We'll just use it here to implement comparison operators
        c = (self.major_version > other.major_version) - (self.major_version < other.major_version)
        if c != 0:
            return c
        c = (self.minor_version > other.minor_version) - (self.minor_version < other.minor_version)
        if c != 0:
            return c
        c = (self.major_build > other.major_build) - (self.major_build < other.major_build)
        if c != 0:
            return c
        return (self.minor_build > other.minor_build) - (self.minor_build < other.minor_build)

    def __eq__(self, other):
        return self.__cmp__(other) == 0

    def __hash__(self):
        return hash(repr(self))

    def __ne__(self, other):
        return self.__cmp__(other) != 0

    def __lt__(self, other):
        return self.__cmp__(other) < 0

    def __le__(self, other):
        return self.__cmp__(other) <= 0

    def __gt__(self, other):
        return self.__cmp__(other) > 0

    def __ge__(self, other):
        return self.__cmp__(other) >= 0

    def __str__(self):
        return f"{self.major_version}.{self.minor_version}.{self.major_build}.{self.minor_build}"

    def __repr__(self):
        return self.__class__.__name__ + repr(
            (self.major_version, self.minor_version, self.major_build, self.minor_build)
        )

Methods

def api_version(self)
Expand source code
def api_version(self):
    for build, api_version, _ in VERSIONS:
        if self.major_version != build.major_version or self.minor_version != build.minor_version:
            continue
        if self >= build:
            return api_version
    raise ValueError(f"API version for build {self} is unknown")
class SupportedVersionClassMixIn (*args, **kwargs)
Expand source code
class SupportedVersionClassMixIn:
    """Supports specifying the supported versions of services, fields, folders etc.

    For distinguished folders, a possibly authoritative source is:
    # https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs
    """

    supported_from = None  # The Exchange build when this element was introduced
    deprecated_from = None  # The Exchange build when this element was deprecated

    @classmethod
    def __new__(cls, *args, **kwargs):
        _check(cls.supported_from, cls.deprecated_from)
        return super().__new__(cls)

    @classmethod
    def supports_version(cls, version):
        return _supports_version(cls.supported_from, cls.deprecated_from, version)

Supports specifying the supported versions of services, fields, folders etc.

For distinguished folders, a possibly authoritative source is:

https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs

Subclasses

Class variables

var deprecated_from
var supported_from

Static methods

def supports_version(version)
class SupportedVersionInstanceMixIn (supported_from=None, deprecated_from=None)
Expand source code
class SupportedVersionInstanceMixIn:
    """Like SupportedVersionClassMixIn but for class instances"""

    def __init__(self, supported_from=None, deprecated_from=None):
        _check(supported_from, deprecated_from)
        self.supported_from = supported_from
        self.deprecated_from = deprecated_from

    def supports_version(self, version):
        return _supports_version(self.supported_from, self.deprecated_from, version)

Like SupportedVersionClassMixIn but for class instances

Subclasses

Methods

def supports_version(self, version)
Expand source code
def supports_version(self, version):
    return _supports_version(self.supported_from, self.deprecated_from, version)
class Version (build, api_version=None)
Expand source code
class Version:
    """Holds information about the server version."""

    __slots__ = "build", "api_version"

    def __init__(self, build, api_version=None):
        if api_version is None:
            if not isinstance(build, Build):
                raise InvalidTypeError("build", build, Build)
            self.api_version = build.api_version()
        else:
            if not isinstance(build, (Build, type(None))):
                raise InvalidTypeError("build", build, Build)
            if not isinstance(api_version, str):
                raise InvalidTypeError("api_version", api_version, str)
            self.api_version = api_version
        self.build = build

    @property
    def fullname(self):
        for build, api_version, full_name in VERSIONS:
            if self.build and (
                self.build.major_version != build.major_version or self.build.minor_version != build.minor_version
            ):
                continue
            if self.api_version == api_version:
                return full_name
        log.warning("Full name for API version %s build %s is unknown", self.api_version, self.build)
        return "UNKNOWN"

    @classmethod
    def guess(cls, protocol, api_version_hint=None):
        """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests
        by hand. We only need a response header containing a ServerVersionInfo element.

        To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that
        without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this
        package supports, until we get a valid response.

        :param protocol:
        :param api_version_hint:  (Default value = None)
        """
        from .properties import ENTRY_ID, EWS_ID, AlternateId
        from .services import ConvertId

        # The protocol doesn't have a version yet, so default to the latest supported version if we don't have a hint.
        api_version = api_version_hint or ConvertId.supported_api_versions()[0]
        log.debug("Asking server for version info using API version %s", api_version)
        # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of
        # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also
        # dangerous.
        protocol.config.version = Version(build=None, api_version=api_version)
        # Use ConvertId as a minimal request to the server to test if the version is correct. If not, ConvertId will
        # try to guess the version automatically. Make sure the call to ConvertId does not require a version build.
        try:
            list(ConvertId(protocol=protocol).call([AlternateId(id="DUMMY", format=EWS_ID, mailbox="DUMMY")], ENTRY_ID))
        except ResponseMessageError as e:
            # We may have survived long enough to get a new version
            if not protocol.config.version.build:
                raise TransportError(f"No valid version headers found in response ({e!r})")
        if not protocol.config.version.build:
            raise TransportError("No valid version headers found in response")
        return protocol.config.version

    @staticmethod
    def _is_invalid_version_string(version):
        # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_
        return re.match(r"V[0-9]{1,4}_.*", version)

    @classmethod
    def from_soap_header(cls, requested_api_version, header):
        info = header.find(f"{{{TNS}}}ServerVersionInfo")
        if info is None:
            info = header.find(f"{{{ANS}}}ServerVersionInfo")
            if info is None:
                raise TransportError(f"No ServerVersionInfo in header: {xml_to_str(header)!r}")
        try:
            build = Build.from_xml(elem=info)
        except ValueError:
            raise TransportError(f"Bad ServerVersionInfo in response: {xml_to_str(header)!r}")
        # Not all Exchange servers send the Version element
        api_version_from_server = info.get("Version") or get_xml_attr(info, f"{{{ANS}}}Version") or build.api_version()
        if api_version_from_server != requested_api_version:
            if cls._is_invalid_version_string(api_version_from_server):
                # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request.
                # Detect these, so we can fall back to a valid version string.
                log.debug(
                    'API version "%s" worked but server reports version "%s". Using "%s"',
                    requested_api_version,
                    api_version_from_server,
                    requested_api_version,
                )
                api_version_from_server = requested_api_version
            else:
                # Trust API version from server response
                log.debug(
                    'API version "%s" worked but server reports version "%s". Using "%s"',
                    requested_api_version,
                    api_version_from_server,
                    api_version_from_server,
                )
        return cls(build=build, api_version=api_version_from_server)

    def copy(self):
        return self.__class__(build=self.build, api_version=self.api_version)

    @classmethod
    def all_versions(cls):
        # Return all supported versions, sorted newest to oldest
        return [cls(build=build, api_version=api_version) for build, api_version, _ in VERSIONS]

    def __hash__(self):
        return hash((self.build, self.api_version))

    def __eq__(self, other):
        if self.api_version != other.api_version:
            return False
        if self.build and not other.build:
            return False
        if other.build and not self.build:
            return False
        return self.build == other.build

    def __repr__(self):
        return self.__class__.__name__ + repr((self.build, self.api_version))

    def __str__(self):
        return f"Build={self.build}, API={self.api_version}, Fullname={self.fullname}"

Holds information about the server version.

Static methods

def all_versions()
def from_soap_header(requested_api_version, header)
def guess(protocol, api_version_hint=None)

Ask the server which version it has. We haven't set up an Account object yet, so we generate requests by hand. We only need a response header containing a ServerVersionInfo element.

To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this package supports, until we get a valid response.

:param protocol: :param api_version_hint: (Default value = None)

Instance variables

var api_version
Expand source code
class Version:
    """Holds information about the server version."""

    __slots__ = "build", "api_version"

    def __init__(self, build, api_version=None):
        if api_version is None:
            if not isinstance(build, Build):
                raise InvalidTypeError("build", build, Build)
            self.api_version = build.api_version()
        else:
            if not isinstance(build, (Build, type(None))):
                raise InvalidTypeError("build", build, Build)
            if not isinstance(api_version, str):
                raise InvalidTypeError("api_version", api_version, str)
            self.api_version = api_version
        self.build = build

    @property
    def fullname(self):
        for build, api_version, full_name in VERSIONS:
            if self.build and (
                self.build.major_version != build.major_version or self.build.minor_version != build.minor_version
            ):
                continue
            if self.api_version == api_version:
                return full_name
        log.warning("Full name for API version %s build %s is unknown", self.api_version, self.build)
        return "UNKNOWN"

    @classmethod
    def guess(cls, protocol, api_version_hint=None):
        """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests
        by hand. We only need a response header containing a ServerVersionInfo element.

        To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that
        without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this
        package supports, until we get a valid response.

        :param protocol:
        :param api_version_hint:  (Default value = None)
        """
        from .properties import ENTRY_ID, EWS_ID, AlternateId
        from .services import ConvertId

        # The protocol doesn't have a version yet, so default to the latest supported version if we don't have a hint.
        api_version = api_version_hint or ConvertId.supported_api_versions()[0]
        log.debug("Asking server for version info using API version %s", api_version)
        # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of
        # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also
        # dangerous.
        protocol.config.version = Version(build=None, api_version=api_version)
        # Use ConvertId as a minimal request to the server to test if the version is correct. If not, ConvertId will
        # try to guess the version automatically. Make sure the call to ConvertId does not require a version build.
        try:
            list(ConvertId(protocol=protocol).call([AlternateId(id="DUMMY", format=EWS_ID, mailbox="DUMMY")], ENTRY_ID))
        except ResponseMessageError as e:
            # We may have survived long enough to get a new version
            if not protocol.config.version.build:
                raise TransportError(f"No valid version headers found in response ({e!r})")
        if not protocol.config.version.build:
            raise TransportError("No valid version headers found in response")
        return protocol.config.version

    @staticmethod
    def _is_invalid_version_string(version):
        # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_
        return re.match(r"V[0-9]{1,4}_.*", version)

    @classmethod
    def from_soap_header(cls, requested_api_version, header):
        info = header.find(f"{{{TNS}}}ServerVersionInfo")
        if info is None:
            info = header.find(f"{{{ANS}}}ServerVersionInfo")
            if info is None:
                raise TransportError(f"No ServerVersionInfo in header: {xml_to_str(header)!r}")
        try:
            build = Build.from_xml(elem=info)
        except ValueError:
            raise TransportError(f"Bad ServerVersionInfo in response: {xml_to_str(header)!r}")
        # Not all Exchange servers send the Version element
        api_version_from_server = info.get("Version") or get_xml_attr(info, f"{{{ANS}}}Version") or build.api_version()
        if api_version_from_server != requested_api_version:
            if cls._is_invalid_version_string(api_version_from_server):
                # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request.
                # Detect these, so we can fall back to a valid version string.
                log.debug(
                    'API version "%s" worked but server reports version "%s". Using "%s"',
                    requested_api_version,
                    api_version_from_server,
                    requested_api_version,
                )
                api_version_from_server = requested_api_version
            else:
                # Trust API version from server response
                log.debug(
                    'API version "%s" worked but server reports version "%s". Using "%s"',
                    requested_api_version,
                    api_version_from_server,
                    api_version_from_server,
                )
        return cls(build=build, api_version=api_version_from_server)

    def copy(self):
        return self.__class__(build=self.build, api_version=self.api_version)

    @classmethod
    def all_versions(cls):
        # Return all supported versions, sorted newest to oldest
        return [cls(build=build, api_version=api_version) for build, api_version, _ in VERSIONS]

    def __hash__(self):
        return hash((self.build, self.api_version))

    def __eq__(self, other):
        if self.api_version != other.api_version:
            return False
        if self.build and not other.build:
            return False
        if other.build and not self.build:
            return False
        return self.build == other.build

    def __repr__(self):
        return self.__class__.__name__ + repr((self.build, self.api_version))

    def __str__(self):
        return f"Build={self.build}, API={self.api_version}, Fullname={self.fullname}"
var build
Expand source code
class Version:
    """Holds information about the server version."""

    __slots__ = "build", "api_version"

    def __init__(self, build, api_version=None):
        if api_version is None:
            if not isinstance(build, Build):
                raise InvalidTypeError("build", build, Build)
            self.api_version = build.api_version()
        else:
            if not isinstance(build, (Build, type(None))):
                raise InvalidTypeError("build", build, Build)
            if not isinstance(api_version, str):
                raise InvalidTypeError("api_version", api_version, str)
            self.api_version = api_version
        self.build = build

    @property
    def fullname(self):
        for build, api_version, full_name in VERSIONS:
            if self.build and (
                self.build.major_version != build.major_version or self.build.minor_version != build.minor_version
            ):
                continue
            if self.api_version == api_version:
                return full_name
        log.warning("Full name for API version %s build %s is unknown", self.api_version, self.build)
        return "UNKNOWN"

    @classmethod
    def guess(cls, protocol, api_version_hint=None):
        """Ask the server which version it has. We haven't set up an Account object yet, so we generate requests
        by hand. We only need a response header containing a ServerVersionInfo element.

        To get API version and build numbers from the server, we need to send a valid SOAP request. We can't do that
        without a valid API version. To solve this chicken-and-egg problem, we try all possible API versions that this
        package supports, until we get a valid response.

        :param protocol:
        :param api_version_hint:  (Default value = None)
        """
        from .properties import ENTRY_ID, EWS_ID, AlternateId
        from .services import ConvertId

        # The protocol doesn't have a version yet, so default to the latest supported version if we don't have a hint.
        api_version = api_version_hint or ConvertId.supported_api_versions()[0]
        log.debug("Asking server for version info using API version %s", api_version)
        # We don't know the build version yet. Hopefully, the server will report it in the SOAP header. Lots of
        # places expect a version to have a build, so this is a bit dangerous, but passing a fake build around is also
        # dangerous.
        protocol.config.version = Version(build=None, api_version=api_version)
        # Use ConvertId as a minimal request to the server to test if the version is correct. If not, ConvertId will
        # try to guess the version automatically. Make sure the call to ConvertId does not require a version build.
        try:
            list(ConvertId(protocol=protocol).call([AlternateId(id="DUMMY", format=EWS_ID, mailbox="DUMMY")], ENTRY_ID))
        except ResponseMessageError as e:
            # We may have survived long enough to get a new version
            if not protocol.config.version.build:
                raise TransportError(f"No valid version headers found in response ({e!r})")
        if not protocol.config.version.build:
            raise TransportError("No valid version headers found in response")
        return protocol.config.version

    @staticmethod
    def _is_invalid_version_string(version):
        # Check if a version string is bogus, e.g. V2_, V2015_ or V2018_
        return re.match(r"V[0-9]{1,4}_.*", version)

    @classmethod
    def from_soap_header(cls, requested_api_version, header):
        info = header.find(f"{{{TNS}}}ServerVersionInfo")
        if info is None:
            info = header.find(f"{{{ANS}}}ServerVersionInfo")
            if info is None:
                raise TransportError(f"No ServerVersionInfo in header: {xml_to_str(header)!r}")
        try:
            build = Build.from_xml(elem=info)
        except ValueError:
            raise TransportError(f"Bad ServerVersionInfo in response: {xml_to_str(header)!r}")
        # Not all Exchange servers send the Version element
        api_version_from_server = info.get("Version") or get_xml_attr(info, f"{{{ANS}}}Version") or build.api_version()
        if api_version_from_server != requested_api_version:
            if cls._is_invalid_version_string(api_version_from_server):
                # For unknown reasons, Office 365 may respond with an API version strings that is invalid in a request.
                # Detect these, so we can fall back to a valid version string.
                log.debug(
                    'API version "%s" worked but server reports version "%s". Using "%s"',
                    requested_api_version,
                    api_version_from_server,
                    requested_api_version,
                )
                api_version_from_server = requested_api_version
            else:
                # Trust API version from server response
                log.debug(
                    'API version "%s" worked but server reports version "%s". Using "%s"',
                    requested_api_version,
                    api_version_from_server,
                    api_version_from_server,
                )
        return cls(build=build, api_version=api_version_from_server)

    def copy(self):
        return self.__class__(build=self.build, api_version=self.api_version)

    @classmethod
    def all_versions(cls):
        # Return all supported versions, sorted newest to oldest
        return [cls(build=build, api_version=api_version) for build, api_version, _ in VERSIONS]

    def __hash__(self):
        return hash((self.build, self.api_version))

    def __eq__(self, other):
        if self.api_version != other.api_version:
            return False
        if self.build and not other.build:
            return False
        if other.build and not self.build:
            return False
        return self.build == other.build

    def __repr__(self):
        return self.__class__.__name__ + repr((self.build, self.api_version))

    def __str__(self):
        return f"Build={self.build}, API={self.api_version}, Fullname={self.fullname}"
prop fullname
Expand source code
@property
def fullname(self):
    for build, api_version, full_name in VERSIONS:
        if self.build and (
            self.build.major_version != build.major_version or self.build.minor_version != build.minor_version
        ):
            continue
        if self.api_version == api_version:
            return full_name
    log.warning("Full name for API version %s build %s is unknown", self.api_version, self.build)
    return "UNKNOWN"

Methods

def copy(self)
Expand source code
def copy(self):
    return self.__class__(build=self.build, api_version=self.api_version)