diff --git a/docs/exchangelib/autodiscover/discovery.html b/docs/exchangelib/autodiscover/discovery.html index 91649b6d..9054f269 100644 --- a/docs/exchangelib/autodiscover/discovery.html +++ b/docs/exchangelib/autodiscover/discovery.html @@ -54,6 +54,13 @@

Module exchangelib.autodiscover.discovery

log = logging.getLogger(__name__) +DNS_LOOKUP_ERRORS = ( + dns.name.EmptyLabel, + dns.resolver.NXDOMAIN, + dns.resolver.NoAnswer, + dns.resolver.NoNameservers, +) + def discover(email, credentials=None, auth_type=None, retry_policy=None): ad_response, protocol = Autodiscovery(email=email, credentials=credentials).discover() @@ -385,8 +392,9 @@

Module exchangelib.autodiscover.discovery

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, dns.name.EmptyLabel): + self.resolver.resolve(f"{hostname}.", "A", lifetime=self.DNS_RESOLVER_ATTRS.get("timeout")) + except DNS_LOOKUP_ERRORS as e: + log.debug("DNS A lookup failure: %s", e) return False return True @@ -406,9 +414,9 @@

Module exchangelib.autodiscover.discovery

log.debug("Attempting to get SRV records for %s", hostname) records = [] try: - answers = self.resolver.resolve(f"{hostname}.", "SRV") - except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e: - log.debug("DNS lookup failure: %s", e) + answers = self.resolver.resolve(f"{hostname}.", "SRV", lifetime=self.DNS_RESOLVER_ATTRS.get("timeout")) + except DNS_LOOKUP_ERRORS as e: + log.debug("DNS SRV lookup failure: %s", e) return records for rdata in answers: try: @@ -959,8 +967,9 @@

Classes

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, dns.name.EmptyLabel): + self.resolver.resolve(f"{hostname}.", "A", lifetime=self.DNS_RESOLVER_ATTRS.get("timeout")) + except DNS_LOOKUP_ERRORS as e: + log.debug("DNS A lookup failure: %s", e) return False return True @@ -980,9 +989,9 @@

Classes

log.debug("Attempting to get SRV records for %s", hostname) records = [] try: - answers = self.resolver.resolve(f"{hostname}.", "SRV") - except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e: - log.debug("DNS lookup failure: %s", e) + answers = self.resolver.resolve(f"{hostname}.", "SRV", lifetime=self.DNS_RESOLVER_ATTRS.get("timeout")) + except DNS_LOOKUP_ERRORS as e: + log.debug("DNS SRV lookup failure: %s", e) return records for rdata in answers: try: diff --git a/docs/exchangelib/autodiscover/index.html b/docs/exchangelib/autodiscover/index.html index cc63d723..abc9faa0 100644 --- a/docs/exchangelib/autodiscover/index.html +++ b/docs/exchangelib/autodiscover/index.html @@ -665,8 +665,9 @@

Inherited members

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, dns.name.EmptyLabel): + self.resolver.resolve(f"{hostname}.", "A", lifetime=self.DNS_RESOLVER_ATTRS.get("timeout")) + except DNS_LOOKUP_ERRORS as e: + log.debug("DNS A lookup failure: %s", e) return False return True @@ -686,9 +687,9 @@

Inherited members

log.debug("Attempting to get SRV records for %s", hostname) records = [] try: - answers = self.resolver.resolve(f"{hostname}.", "SRV") - except (dns.resolver.NoNameservers, dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) as e: - log.debug("DNS lookup failure: %s", e) + answers = self.resolver.resolve(f"{hostname}.", "SRV", lifetime=self.DNS_RESOLVER_ATTRS.get("timeout")) + except DNS_LOOKUP_ERRORS as e: + log.debug("DNS SRV lookup failure: %s", e) return records for rdata in answers: try: diff --git a/docs/exchangelib/configuration.html b/docs/exchangelib/configuration.html index 1cf59d9b..5feb396b 100644 --- a/docs/exchangelib/configuration.html +++ b/docs/exchangelib/configuration.html @@ -89,12 +89,12 @@

Module exchangelib.configuration

if auth_type is None: # Set a default auth type for the credentials where this makes sense auth_type = DEFAULT_AUTH_TYPE.get(type(credentials)) - elif credentials is None and auth_type in CREDENTIALS_REQUIRED: + if auth_type is not None and auth_type not in AUTH_TYPE_MAP: + raise InvalidEnumValue("auth_type", auth_type, AUTH_TYPE_MAP) + if credentials is None and auth_type in CREDENTIALS_REQUIRED: raise ValueError(f"Auth type {auth_type!r} was detected but no credentials were provided") if server and service_endpoint: raise AttributeError("Only one of 'server' or 'service_endpoint' must be provided") - if auth_type is not None and auth_type not in AUTH_TYPE_MAP: - raise InvalidEnumValue("auth_type", auth_type, AUTH_TYPE_MAP) if not retry_policy: retry_policy = FailFast() if not isinstance(version, (Version, type(None))): @@ -212,12 +212,12 @@

Classes

if auth_type is None: # Set a default auth type for the credentials where this makes sense auth_type = DEFAULT_AUTH_TYPE.get(type(credentials)) - elif credentials is None and auth_type in CREDENTIALS_REQUIRED: + if auth_type is not None and auth_type not in AUTH_TYPE_MAP: + raise InvalidEnumValue("auth_type", auth_type, AUTH_TYPE_MAP) + if credentials is None and auth_type in CREDENTIALS_REQUIRED: raise ValueError(f"Auth type {auth_type!r} was detected but no credentials were provided") if server and service_endpoint: raise AttributeError("Only one of 'server' or 'service_endpoint' must be provided") - if auth_type is not None and auth_type not in AUTH_TYPE_MAP: - raise InvalidEnumValue("auth_type", auth_type, AUTH_TYPE_MAP) if not retry_policy: retry_policy = FailFast() if not isinstance(version, (Version, type(None))): diff --git a/docs/exchangelib/credentials.html b/docs/exchangelib/credentials.html index 9543bca5..f3d4c078 100644 --- a/docs/exchangelib/credentials.html +++ b/docs/exchangelib/credentials.html @@ -141,21 +141,21 @@

Module exchangelib.credentials

the associated auth code grant type for multi-tenant applications. """ - def __init__(self, client_id, client_secret, tenant_id=None, identity=None): + def __init__(self, client_id, client_secret, tenant_id=None, identity=None, access_token=None): """ :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing :param client_secret: Secret associated with the OAuth application :param tenant_id: Microsoft tenant ID of the account to access :param identity: An Identity object representing the account that these credentials are connected to. + :param access_token: Previously-obtained access token, as a dict or an oauthlib.oauth2.OAuth2Token """ super().__init__() self.client_id = client_id self.client_secret = client_secret self.tenant_id = tenant_id self.identity = identity - # When set, access_token is a dict (or an oauthlib.oauth2.OAuth2Token, which is also a dict) - self.access_token = None + self.access_token = access_token def refresh(self, session): # Creating a new session gets a new access token, so there's no work here to refresh the credentials. This @@ -207,8 +207,8 @@

Module exchangelib.credentials

several ways: * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if supplied with a refresh token. - * Given an existing access token, refresh token, client ID, and client secret, use the access token until it - expires and then refresh it as needed. + * Given an existing access token, client ID, and client secret, use the access token until it expires and then + refresh it as needed. * Given only an existing access token, use it until it expires. This can be used to let the calling application refresh tokens itself by subclassing and implementing refresh(). @@ -217,7 +217,7 @@

Module exchangelib.credentials

tenant. """ - def __init__(self, authorization_code=None, access_token=None, **kwargs): + def __init__(self, authorization_code=None, access_token=None, client_id=None, client_secret=None, **kwargs): """ :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing @@ -229,7 +229,7 @@

Module exchangelib.credentials

:param access_token: Previously-obtained access token. If a token exists and the application will handle refreshing by itself (or opts not to handle it), this parameter alone is sufficient. """ - super().__init__(**kwargs) + super().__init__(client_id=client_id, client_secret=client_secret, **kwargs) self.authorization_code = authorization_code if access_token is not None and not isinstance(access_token, dict): raise InvalidTypeError("access_token", access_token, OAuth2Token) @@ -442,15 +442,15 @@

Inherited members

class OAuth2AuthorizationCodeCredentials -(authorization_code=None, access_token=None, **kwargs) +(authorization_code=None, access_token=None, client_id=None, client_secret=None, **kwargs)

Login info for OAuth 2.0 authentication using the authorization code grant type. This can be used in one of several ways: * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if supplied with a refresh token. -* Given an existing access token, refresh token, client ID, and client secret, use the access token until it -expires and then refresh it as needed. +* Given an existing access token, client ID, and client secret, use the access token until it expires and then +refresh it as needed. * Given only an existing access token, use it until it expires. This can be used to let the calling application refresh tokens itself by subclassing and implementing refresh().

Unlike the base (client credentials) grant, authorization code credentials don't require a Microsoft tenant ID @@ -473,8 +473,8 @@

Inherited members

several ways: * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if supplied with a refresh token. - * Given an existing access token, refresh token, client ID, and client secret, use the access token until it - expires and then refresh it as needed. + * Given an existing access token, client ID, and client secret, use the access token until it expires and then + refresh it as needed. * Given only an existing access token, use it until it expires. This can be used to let the calling application refresh tokens itself by subclassing and implementing refresh(). @@ -483,7 +483,7 @@

Inherited members

tenant. """ - def __init__(self, authorization_code=None, access_token=None, **kwargs): + def __init__(self, authorization_code=None, access_token=None, client_id=None, client_secret=None, **kwargs): """ :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing @@ -495,7 +495,7 @@

Inherited members

:param access_token: Previously-obtained access token. If a token exists and the application will handle refreshing by itself (or opts not to handle it), this parameter alone is sufficient. """ - super().__init__(**kwargs) + super().__init__(client_id=client_id, client_secret=client_secret, **kwargs) self.authorization_code = authorization_code if access_token is not None and not isinstance(access_token, dict): raise InvalidTypeError("access_token", access_token, OAuth2Token) @@ -533,7 +533,7 @@

Inherited members

class OAuth2Credentials -(client_id, client_secret, tenant_id=None, identity=None) +(client_id, client_secret, tenant_id=None, identity=None, access_token=None)

Login info for OAuth 2.0 client credentials authentication, as well as a base for other OAuth 2.0 grant types.

@@ -544,7 +544,8 @@

Inherited members

:param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing :param client_secret: Secret associated with the OAuth application :param tenant_id: Microsoft tenant ID of the account to access -:param identity: An Identity object representing the account that these credentials are connected to.

+:param identity: An Identity object representing the account that these credentials are connected to. +:param access_token: Previously-obtained access token, as a dict or an oauthlib.oauth2.OAuth2Token

Expand source code @@ -558,21 +559,21 @@

Inherited members

the associated auth code grant type for multi-tenant applications. """ - def __init__(self, client_id, client_secret, tenant_id=None, identity=None): + def __init__(self, client_id, client_secret, tenant_id=None, identity=None, access_token=None): """ :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing :param client_secret: Secret associated with the OAuth application :param tenant_id: Microsoft tenant ID of the account to access :param identity: An Identity object representing the account that these credentials are connected to. + :param access_token: Previously-obtained access token, as a dict or an oauthlib.oauth2.OAuth2Token """ super().__init__() self.client_id = client_id self.client_secret = client_secret self.tenant_id = tenant_id self.identity = identity - # When set, access_token is a dict (or an oauthlib.oauth2.OAuth2Token, which is also a dict) - self.access_token = None + self.access_token = access_token def refresh(self, session): # Creating a new session gets a new access token, so there's no work here to refresh the credentials. This diff --git a/docs/exchangelib/errors.html b/docs/exchangelib/errors.html index 704a5af7..dc7c9c6b 100644 --- a/docs/exchangelib/errors.html +++ b/docs/exchangelib/errors.html @@ -149,10 +149,6 @@

Module exchangelib.errors

pass -class TimezoneDefinitionInvalidForYear(EWSError): - pass - - class SessionPoolMinSizeReached(EWSError): pass @@ -1446,6 +1442,10 @@

Module exchangelib.errors

pass +class ErrorRecoverableItemsAccessDenied(ResponseMessageError): + pass + + class ErrorRecurrenceEndDateTooBig(ResponseMessageError): pass @@ -1720,7 +1720,7 @@

Module exchangelib.errors

pass -# Microsoft recommends to cache the autodiscover data around 24 hours and perform autodiscover +# Microsoft recommends caching the autodiscover data around 24 hours and perform autodiscover # immediately following certain error responses from EWS. See more at # http://blogs.msdn.com/b/mstehle/archive/2010/11/09/ews-best-practices-use-autodiscover.aspx @@ -1903,7 +1903,6 @@

Subclasses

  • EWSWarning
  • SessionPoolMaxSizeReached
  • SessionPoolMinSizeReached
  • -
  • TimezoneDefinitionInvalidForYear
  • TransportError
  • UnauthorizedError
  • UnknownTimeZone
  • @@ -8881,6 +8880,28 @@

    Ancestors

  • builtins.BaseException
  • +
    +class ErrorRecoverableItemsAccessDenied +(value) +
    +
    +

    Global error type within this module.

    +
    + +Expand source code + +
    class ErrorRecoverableItemsAccessDenied(ResponseMessageError):
    +    pass
    +
    +

    Ancestors

    + +
    class ErrorRecurrenceEndDateTooBig (value) @@ -10914,6 +10935,7 @@

    Subclasses

  • ErrorQuotaExceeded
  • ErrorReadEventsFailed
  • ErrorReadReceiptNotPending
  • +
  • ErrorRecoverableItemsAccessDenied
  • ErrorRecurrenceEndDateTooBig
  • ErrorRecurrenceHasNoOccurrence
  • ErrorRemoveDelegatesFailed
  • @@ -11045,26 +11067,6 @@

    Ancestors

  • builtins.BaseException
  • -
    -class TimezoneDefinitionInvalidForYear -(value) -
    -
    -

    Global error type within this module.

    -
    - -Expand source code - -
    class TimezoneDefinitionInvalidForYear(EWSError):
    -    pass
    -
    -

    Ancestors

    - -
    class TransportError (value) @@ -12121,6 +12123,9 @@

    ErrorReadReceiptNotPending

  • +

    ErrorRecoverableItemsAccessDenied

    +
  • +
  • ErrorRecurrenceEndDateTooBig

  • @@ -12361,9 +12366,6 @@

    SessionPoolMinSizeReached

  • -

    TimezoneDefinitionInvalidForYear

    -
  • -
  • TransportError

  • diff --git a/docs/exchangelib/ewsdatetime.html b/docs/exchangelib/ewsdatetime.html index f10a28fe..36fce92e 100644 --- a/docs/exchangelib/ewsdatetime.html +++ b/docs/exchangelib/ewsdatetime.html @@ -953,7 +953,7 @@

    Methods

    Ancestors

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

    Class variables

    diff --git a/docs/exchangelib/fields.html b/docs/exchangelib/fields.html index e67f562f..a52f863d 100644 --- a/docs/exchangelib/fields.html +++ b/docs/exchangelib/fields.html @@ -6611,7 +6611,10 @@

    Class variables

    var value_cls
    -

    Difference between two datetime values.

    +

    Difference between two datetime values.

    +

    timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)

    +

    All arguments are optional and default to 0. +Arguments may be integers or floats, and may be positive or negative.

    Methods

    diff --git a/docs/exchangelib/folders/base.html b/docs/exchangelib/folders/base.html index df412a1b..7d1b336a 100644 --- a/docs/exchangelib/folders/base.html +++ b/docs/exchangelib/folders/base.html @@ -38,6 +38,7 @@

    Module exchangelib.folders.base

    ErrorDeleteDistinguishedFolder, ErrorFolderNotFound, ErrorItemNotFound, + ErrorRecoverableItemsAccessDenied, InvalidTypeError, ) from ..fields import ( @@ -75,6 +76,13 @@

    Module exchangelib.folders.base

    log = logging.getLogger(__name__) +DELETE_FOLDER_ERRORS = ( + ErrorAccessDenied, + ErrorCannotDeleteObject, + ErrorCannotEmptyFolder, + ErrorItemNotFound, +) + class BaseFolder(RegisterMixIn, SearchableMixIn, metaclass=EWSMeta): """Base class for all classes that implement a folder.""" @@ -257,27 +265,41 @@

    Module exchangelib.folders.base

    :return: """ from .known_folders import ( + ApplicationData, Calendar, Contacts, ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, Messages, RecipientCache, + RecoveryPoints, Reminders, RSSFeeds, + Signal, + SwssItems, Tasks, ) for folder_cls in ( - Messages, - Tasks, + ApplicationData, Calendar, - ConversationSettings, Contacts, + ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, - Reminders, - RecipientCache, + Messages, RSSFeeds, + RecipientCache, + RecoveryPoints, + Reminders, + Signal, + SwssItems, + Tasks, ): if folder_cls.CONTAINER_CLASS == container_class: return folder_cls @@ -430,12 +452,21 @@

    Module exchangelib.folders.base

    def wipe(self, page_size=None, chunk_size=None, _seen=None, _level=0): # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect # distinguished folders from being deleted. Use with caution! + from .known_folders import Audits + _seen = _seen or set() if self.id in _seen: raise RecursionError(f"We already tried to wipe {self}") if _level > 16: raise RecursionError(f"Max recursion level reached: {_level}") _seen.add(self.id) + if isinstance(self, Audits): + # Shortcircuit because this folder can have many items that are all non-deletable + log.warning("Cannot wipe audits folder %s", self) + return + if self.is_distinguished and "recoverableitems" in self.DISTINGUISHED_FOLDER_ID: + log.warning("Cannot wipe recoverable items folder %s", self) + return log.warning("Wiping %s", self) has_distinguished_subfolders = any(f.is_distinguished for f in self.children) try: @@ -443,12 +474,15 @@

    Module exchangelib.folders.base

    self.empty(delete_sub_folders=False) else: self.empty(delete_sub_folders=True) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except ErrorRecoverableItemsAccessDenied: + log.warning("Access denied to %s. Skipping", self) + return + except DELETE_FOLDER_ERRORS: try: if has_distinguished_subfolders: raise # We already tried this self.empty(delete_sub_folders=False) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to empty %s. Trying to delete items instead", self) kwargs = {} if page_size is not None: @@ -457,7 +491,7 @@

    Module exchangelib.folders.base

    kwargs["chunk_size"] = chunk_size try: self.all().delete(**kwargs) - except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to delete items in %s", self) _level += 1 for f in self.children: @@ -1127,27 +1161,41 @@

    Classes

    :return: """ from .known_folders import ( + ApplicationData, Calendar, Contacts, ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, Messages, RecipientCache, + RecoveryPoints, Reminders, RSSFeeds, + Signal, + SwssItems, Tasks, ) for folder_cls in ( - Messages, - Tasks, + ApplicationData, Calendar, - ConversationSettings, Contacts, + ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, - Reminders, - RecipientCache, + Messages, RSSFeeds, + RecipientCache, + RecoveryPoints, + Reminders, + Signal, + SwssItems, + Tasks, ): if folder_cls.CONTAINER_CLASS == container_class: return folder_cls @@ -1300,12 +1348,21 @@

    Classes

    def wipe(self, page_size=None, chunk_size=None, _seen=None, _level=0): # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect # distinguished folders from being deleted. Use with caution! + from .known_folders import Audits + _seen = _seen or set() if self.id in _seen: raise RecursionError(f"We already tried to wipe {self}") if _level > 16: raise RecursionError(f"Max recursion level reached: {_level}") _seen.add(self.id) + if isinstance(self, Audits): + # Shortcircuit because this folder can have many items that are all non-deletable + log.warning("Cannot wipe audits folder %s", self) + return + if self.is_distinguished and "recoverableitems" in self.DISTINGUISHED_FOLDER_ID: + log.warning("Cannot wipe recoverable items folder %s", self) + return log.warning("Wiping %s", self) has_distinguished_subfolders = any(f.is_distinguished for f in self.children) try: @@ -1313,12 +1370,15 @@

    Classes

    self.empty(delete_sub_folders=False) else: self.empty(delete_sub_folders=True) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except ErrorRecoverableItemsAccessDenied: + log.warning("Access denied to %s. Skipping", self) + return + except DELETE_FOLDER_ERRORS: try: if has_distinguished_subfolders: raise # We already tried this self.empty(delete_sub_folders=False) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to empty %s. Trying to delete items instead", self) kwargs = {} if page_size is not None: @@ -1327,7 +1387,7 @@

    Classes

    kwargs["chunk_size"] = chunk_size try: self.all().delete(**kwargs) - except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to delete items in %s", self) _level += 1 for f in self.children: @@ -1777,27 +1837,41 @@

    Static methods

    :return: """ from .known_folders import ( + ApplicationData, Calendar, Contacts, ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, Messages, RecipientCache, + RecoveryPoints, Reminders, RSSFeeds, + Signal, + SwssItems, Tasks, ) for folder_cls in ( - Messages, - Tasks, + ApplicationData, Calendar, - ConversationSettings, Contacts, + ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, - Reminders, - RecipientCache, + Messages, RSSFeeds, + RecipientCache, + RecoveryPoints, + Reminders, + Signal, + SwssItems, + Tasks, ): if folder_cls.CONTAINER_CLASS == container_class: return folder_cls @@ -2788,12 +2862,21 @@

    Methods

    def wipe(self, page_size=None, chunk_size=None, _seen=None, _level=0):
         # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
         # distinguished folders from being deleted. Use with caution!
    +    from .known_folders import Audits
    +
         _seen = _seen or set()
         if self.id in _seen:
             raise RecursionError(f"We already tried to wipe {self}")
         if _level > 16:
             raise RecursionError(f"Max recursion level reached: {_level}")
         _seen.add(self.id)
    +    if isinstance(self, Audits):
    +        # Shortcircuit because this folder can have many items that are all non-deletable
    +        log.warning("Cannot wipe audits folder %s", self)
    +        return
    +    if self.is_distinguished and "recoverableitems" in self.DISTINGUISHED_FOLDER_ID:
    +        log.warning("Cannot wipe recoverable items folder %s", self)
    +        return
         log.warning("Wiping %s", self)
         has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
         try:
    @@ -2801,12 +2884,15 @@ 

    Methods

    self.empty(delete_sub_folders=False) else: self.empty(delete_sub_folders=True) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except ErrorRecoverableItemsAccessDenied: + log.warning("Access denied to %s. Skipping", self) + return + except DELETE_FOLDER_ERRORS: try: if has_distinguished_subfolders: raise # We already tried this self.empty(delete_sub_folders=False) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to empty %s. Trying to delete items instead", self) kwargs = {} if page_size is not None: @@ -2815,7 +2901,7 @@

    Methods

    kwargs["chunk_size"] = chunk_size try: self.all().delete(**kwargs) - except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to delete items in %s", self) _level += 1 for f in self.children: @@ -3005,17 +3091,22 @@

    Ancestors

    Subclasses

    • AllItems
    • +
    • ApplicationData
    • Audits
    • +
    • Birthdays
    • Calendar
    • CalendarLogging
    • CommonViews
    • Contacts
    • ConversationSettings
    • +
    • CrawlerData
    • DefaultFoldersChangeHistory
    • DeferredAction
    • DeletedItems
    • +
    • DlpPolicyEvaluation
    • ExchangeSyncData
    • Files
    • +
    • FreeBusyCache
    • FreebusyData
    • GraphAnalytics
    • Location
    • @@ -3025,13 +3116,16 @@

      Subclasses

    • PassThroughSearchResults
    • PdpProfileV2Secured
    • RSSFeeds
    • +
    • RecoveryPoints
    • Reminders
    • Schedule
    • Sharing
    • Shortcuts
    • Signal
    • +
    • SkypeTeamsMessages
    • SmsAndChatsSync
    • SpoolerQueue
    • +
    • SwssItems
    • System
    • System1
    • Tasks
    • diff --git a/docs/exchangelib/folders/index.html b/docs/exchangelib/folders/index.html index d29c8485..af9ef9c2 100644 --- a/docs/exchangelib/folders/index.html +++ b/docs/exchangelib/folders/index.html @@ -34,6 +34,7 @@

      Module exchangelib.folders

      AdminAuditLogs, AllContacts, AllItems, + ApplicationData, ArchiveDeletedItems, ArchiveInbox, ArchiveMsgFolderRoot, @@ -42,6 +43,7 @@

      Module exchangelib.folders

      ArchiveRecoverableItemsRoot, ArchiveRecoverableItemsVersions, Audits, + Birthdays, Calendar, CalendarLogging, CommonViews, @@ -50,14 +52,17 @@

      Module exchangelib.folders

      Contacts, ConversationHistory, ConversationSettings, + CrawlerData, DefaultFoldersChangeHistory, DeferredAction, DeletedItems, Directory, + DlpPolicyEvaluation, Drafts, ExchangeSyncData, Favorites, Files, + FreeBusyCache, FreebusyData, Friends, GALContacts, @@ -88,6 +93,7 @@

      Module exchangelib.folders

      RecoverableItemsPurges, RecoverableItemsRoot, RecoverableItemsVersions, + RecoveryPoints, Reminders, RSSFeeds, Schedule, @@ -97,8 +103,10 @@

      Module exchangelib.folders

      Sharing, Shortcuts, Signal, + SkypeTeamsMessages, SmsAndChatsSync, SpoolerQueue, + SwssItems, SyncIssues, System, Tasks, @@ -113,14 +121,10 @@

      Module exchangelib.folders

      from .roots import ArchiveRoot, PublicFoldersRoot, Root, RootOfHierarchy __all__ = [ - "FolderId", - "DistinguishedFolderId", - "FolderCollection", - "BaseFolder", - "Folder", "AdminAuditLogs", "AllContacts", "AllItems", + "ApplicationData", "ArchiveDeletedItems", "ArchiveInbox", "ArchiveMsgFolderRoot", @@ -128,22 +132,36 @@

      Module exchangelib.folders

      "ArchiveRecoverableItemsPurges", "ArchiveRecoverableItemsRoot", "ArchiveRecoverableItemsVersions", + "ArchiveRoot", "Audits", + "BaseFolder", + "Birthdays", "Calendar", "CalendarLogging", "CommonViews", + "Companies", "Conflicts", "Contacts", "ConversationHistory", "ConversationSettings", + "CrawlerData", + "DEEP", "DefaultFoldersChangeHistory", "DeferredAction", "DeletedItems", "Directory", + "DistinguishedFolderId", + "DlpPolicyEvaluation", "Drafts", "ExchangeSyncData", + "FOLDER_TRAVERSAL_CHOICES", "Favorites", "Files", + "Folder", + "FolderCollection", + "FolderId", + "FolderQuerySet", + "FreeBusyCache", "FreebusyData", "Friends", "GALContacts", @@ -159,13 +177,17 @@

      Module exchangelib.folders

      "MsgFolderRoot", "MyContacts", "MyContactsExtended", + "NON_DELETABLE_FOLDERS", "NonDeletableFolderMixin", "Notes", + "OrganizationalContacts", "Outbox", "ParkedMessages", "PassThroughSearchResults", "PdpProfileV2Secured", + "PeopleCentricConversationBuddies", "PeopleConnect", + "PublicFoldersRoot", "QuickContacts", "RSSFeeds", "RecipientCache", @@ -173,7 +195,12 @@

      Module exchangelib.folders

      "RecoverableItemsPurges", "RecoverableItemsRoot", "RecoverableItemsVersions", + "RecoveryPoints", "Reminders", + "Root", + "RootOfHierarchy", + "SHALLOW", + "SOFT_DELETED", "Schedule", "SearchFolders", "SentItems", @@ -181,8 +208,11 @@

      Module exchangelib.folders

      "Sharing", "Shortcuts", "Signal", + "SingleFolderQuerySet", + "SkypeTeamsMessages", "SmsAndChatsSync", "SpoolerQueue", + "SwssItems", "SyncIssues", "System", "Tasks", @@ -192,20 +222,6 @@

      Module exchangelib.folders

      "VoiceMail", "WellknownFolder", "WorkingSet", - "Companies", - "OrganizationalContacts", - "PeopleCentricConversationBuddies", - "NON_DELETABLE_FOLDERS", - "FolderQuerySet", - "SingleFolderQuerySet", - "FOLDER_TRAVERSAL_CHOICES", - "SHALLOW", - "DEEP", - "SOFT_DELETED", - "Root", - "ArchiveRoot", - "PublicFoldersRoot", - "RootOfHierarchy", ]
    @@ -475,6 +491,75 @@

    Inherited members

  • +
    +class ApplicationData +(**kwargs) +
    +
    +

    A mixin for non-wellknown folders than that are not deletable.

    +
    + +Expand source code + +
    class ApplicationData(NonDeletableFolderMixin, Folder):
    +    CONTAINER_CLASS = "IPM.ApplicationData"
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var CONTAINER_CLASS
    +
    +
    +
    +
    +

    Inherited members

    + +
    class ArchiveDeletedItems (**kwargs) @@ -1342,27 +1427,41 @@

    Inherited members

    :return: """ from .known_folders import ( + ApplicationData, Calendar, Contacts, ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, Messages, RecipientCache, + RecoveryPoints, Reminders, RSSFeeds, + Signal, + SwssItems, Tasks, ) for folder_cls in ( - Messages, - Tasks, + ApplicationData, Calendar, - ConversationSettings, Contacts, + ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, - Reminders, - RecipientCache, + Messages, RSSFeeds, + RecipientCache, + RecoveryPoints, + Reminders, + Signal, + SwssItems, + Tasks, ): if folder_cls.CONTAINER_CLASS == container_class: return folder_cls @@ -1515,12 +1614,21 @@

    Inherited members

    def wipe(self, page_size=None, chunk_size=None, _seen=None, _level=0): # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect # distinguished folders from being deleted. Use with caution! + from .known_folders import Audits + _seen = _seen or set() if self.id in _seen: raise RecursionError(f"We already tried to wipe {self}") if _level > 16: raise RecursionError(f"Max recursion level reached: {_level}") _seen.add(self.id) + if isinstance(self, Audits): + # Shortcircuit because this folder can have many items that are all non-deletable + log.warning("Cannot wipe audits folder %s", self) + return + if self.is_distinguished and "recoverableitems" in self.DISTINGUISHED_FOLDER_ID: + log.warning("Cannot wipe recoverable items folder %s", self) + return log.warning("Wiping %s", self) has_distinguished_subfolders = any(f.is_distinguished for f in self.children) try: @@ -1528,12 +1636,15 @@

    Inherited members

    self.empty(delete_sub_folders=False) else: self.empty(delete_sub_folders=True) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except ErrorRecoverableItemsAccessDenied: + log.warning("Access denied to %s. Skipping", self) + return + except DELETE_FOLDER_ERRORS: try: if has_distinguished_subfolders: raise # We already tried this self.empty(delete_sub_folders=False) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to empty %s. Trying to delete items instead", self) kwargs = {} if page_size is not None: @@ -1542,7 +1653,7 @@

    Inherited members

    kwargs["chunk_size"] = chunk_size try: self.all().delete(**kwargs) - except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to delete items in %s", self) _level += 1 for f in self.children: @@ -1992,27 +2103,41 @@

    Static methods

    :return: """ from .known_folders import ( + ApplicationData, Calendar, Contacts, ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, Messages, RecipientCache, + RecoveryPoints, Reminders, RSSFeeds, + Signal, + SwssItems, Tasks, ) for folder_cls in ( - Messages, - Tasks, + ApplicationData, Calendar, - ConversationSettings, Contacts, + ConversationSettings, + CrawlerData, + DlpPolicyEvaluation, + FreeBusyCache, GALContacts, - Reminders, - RecipientCache, + Messages, RSSFeeds, + RecipientCache, + RecoveryPoints, + Reminders, + Signal, + SwssItems, + Tasks, ): if folder_cls.CONTAINER_CLASS == container_class: return folder_cls @@ -3003,12 +3128,21 @@

    Methods

    def wipe(self, page_size=None, chunk_size=None, _seen=None, _level=0):
         # Recursively deletes all items in this folder, and all subfolders and their content. Attempts to protect
         # distinguished folders from being deleted. Use with caution!
    +    from .known_folders import Audits
    +
         _seen = _seen or set()
         if self.id in _seen:
             raise RecursionError(f"We already tried to wipe {self}")
         if _level > 16:
             raise RecursionError(f"Max recursion level reached: {_level}")
         _seen.add(self.id)
    +    if isinstance(self, Audits):
    +        # Shortcircuit because this folder can have many items that are all non-deletable
    +        log.warning("Cannot wipe audits folder %s", self)
    +        return
    +    if self.is_distinguished and "recoverableitems" in self.DISTINGUISHED_FOLDER_ID:
    +        log.warning("Cannot wipe recoverable items folder %s", self)
    +        return
         log.warning("Wiping %s", self)
         has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
         try:
    @@ -3016,12 +3150,15 @@ 

    Methods

    self.empty(delete_sub_folders=False) else: self.empty(delete_sub_folders=True) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except ErrorRecoverableItemsAccessDenied: + log.warning("Access denied to %s. Skipping", self) + return + except DELETE_FOLDER_ERRORS: try: if has_distinguished_subfolders: raise # We already tried this self.empty(delete_sub_folders=False) - except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to empty %s. Trying to delete items instead", self) kwargs = {} if page_size is not None: @@ -3030,7 +3167,7 @@

    Methods

    kwargs["chunk_size"] = chunk_size try: self.all().delete(**kwargs) - except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound): + except DELETE_FOLDER_ERRORS: log.warning("Not allowed to delete items in %s", self) _level += 1 for f in self.children: @@ -3069,6 +3206,82 @@

    Inherited members

    +
    +class Birthdays +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class Birthdays(Folder):
    +    CONTAINER_CLASS = "IPF.Appointment.Birthday"
    +    LOCALIZED_NAMES = {
    +        None: ("Birthdays",),
    +        "da_DK": ("Fødselsdage",),
    +    }
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var CONTAINER_CLASS
    +
    +
    +
    +
    var LOCALIZED_NAMES
    +
    +
    +
    +
    +

    Inherited members

    + +
    class Calendar (**kwargs) @@ -3346,6 +3559,7 @@

    Inherited members

    CONTAINTER_CLASS = "IPF.Contact.Company" LOCALIZED_NAMES = { None: ("Companies",), + "da_DK": ("Firmaer",), }

    Ancestors

    @@ -3742,6 +3956,74 @@

    Inherited members

    +
    +class CrawlerData +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class CrawlerData(Folder):
    +    CONTAINER_CLASS = "IPF.StoreItem.CrawlerData"
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var CONTAINER_CLASS
    +
    +
    +
    +
    +

    Inherited members

    + +
    class DefaultFoldersChangeHistory (**kwargs) @@ -4140,6 +4422,74 @@

    Inherited members

    +
    +class DlpPolicyEvaluation +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class DlpPolicyEvaluation(Folder):
    +    CONTAINER_CLASS = "IPF.StoreItem.DlpPolicyEvaluation"
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var CONTAINER_CLASS
    +
    +
    +
    +
    +

    Inherited members

    + +
    class Drafts (**kwargs) @@ -4603,17 +4953,22 @@

    Ancestors

    Subclasses

    +
    +class SkypeTeamsMessages +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class SkypeTeamsMessages(Folder):
    +    CONTAINER_CLASS = "IPF.SkypeTeams.Message"
    +    LOCALIZED_NAMES = {
    +        None: ("Team-chat",),
    +    }
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var CONTAINER_CLASS
    +
    +
    +
    +
    var LOCALIZED_NAMES
    +
    +
    +
    +
    +

    Inherited members

    + +
    class SmsAndChatsSync (**kwargs) @@ -10373,6 +10946,74 @@

    Inherited members

    +
    +class SwssItems +(**kwargs) +
    +
    + +
    + +Expand source code + +
    class SwssItems(Folder):
    +    CONTAINER_CLASS = "IPF.StoreItem.SwssItems"
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var CONTAINER_CLASS
    +
    +
    +
    +
    +

    Inherited members

    + +
    class SyncIssues (**kwargs) @@ -11153,6 +11794,12 @@

    ApplicationData

    + + +
  • ArchiveDeletedItems

    • DISTINGUISHED_FOLDER_ID
    • @@ -11290,6 +11937,13 @@

      Birthdays

      + + +
    • Calendar

      +
      +class SwssItems +(**kwargs) +
      +
      + +
      + +Expand source code + +
      class SwssItems(Folder):
      +    CONTAINER_CLASS = "IPF.StoreItem.SwssItems"
      +
      +

      Ancestors

      + +

      Class variables

      +
      +
      var CONTAINER_CLASS
      +
      +
      +
      +
      +

      Inherited members

      + +
      class SyncIssues (**kwargs) @@ -6859,6 +7476,12 @@

      ApplicationData

      + +
    • +
    • ArchiveDeletedItems

      • DISTINGUISHED_FOLDER_ID
      • @@ -6915,6 +7538,13 @@

        Birthdays

        + + +
      • Calendar

        • CONTAINER_CLASS
        • @@ -6976,6 +7606,12 @@

        • +

          CrawlerData

          + +
        • +
        • DefaultFoldersChangeHistory

          • CONTAINER_CLASS
          • @@ -7005,6 +7641,12 @@

            DlpPolicyEvaluation

            + + +
          • Drafts

            • DISTINGUISHED_FOLDER_ID
            • @@ -7033,6 +7675,12 @@

              FreeBusyCache

              + + +
            • FreebusyData

              • LOCALIZED_NAMES
              • @@ -7255,6 +7903,12 @@

              • +

                RecoveryPoints

                + +
              • +
              • Reminders

                • CONTAINER_CLASS
                • @@ -7308,6 +7962,13 @@

                  SkypeTeamsMessages

                  + + +
                • SmsAndChatsSync

                  • CONTAINER_CLASS
                  • @@ -7321,6 +7982,12 @@

                    SwssItems

                    + + +
                  • SyncIssues

                    • CONTAINER_CLASS
                    • diff --git a/docs/exchangelib/folders/roots.html b/docs/exchangelib/folders/roots.html index 557f7715..713a1971 100644 --- a/docs/exchangelib/folders/roots.html +++ b/docs/exchangelib/folders/roots.html @@ -36,6 +36,7 @@

                      Module exchangelib.folders.roots

                      from .base import BaseFolder from .collections import FolderCollection from .known_folders import ( + MISC_FOLDERS, NON_DELETABLE_FOLDERS, WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT, WELLKNOWN_FOLDERS_IN_ROOT, @@ -233,7 +234,7 @@

                      Module exchangelib.folders.roots

                      :param folder_name: :param locale: a string, e.g. 'da_DK' """ - for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS: + for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS + MISC_FOLDERS: if folder_name.lower() in folder_cls.localized_names(locale): return folder_cls raise KeyError() @@ -984,7 +985,7 @@

                      Inherited members

                      :param folder_name: :param locale: a string, e.g. 'da_DK' """ - for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS: + for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS + MISC_FOLDERS: if folder_name.lower() in folder_cls.localized_names(locale): return folder_cls raise KeyError() @@ -1050,7 +1051,7 @@

                      Static methods

                      :param folder_name: :param locale: a string, e.g. 'da_DK' """ - for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS: + for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS + MISC_FOLDERS: if folder_name.lower() in folder_cls.localized_names(locale): return folder_cls raise KeyError()
                      diff --git a/docs/exchangelib/index.html b/docs/exchangelib/index.html index 53a8450a..0cdb0afa 100644 --- a/docs/exchangelib/index.html +++ b/docs/exchangelib/index.html @@ -56,7 +56,7 @@

                      Package exchangelib

                      from .transport import BASIC, CBA, DIGEST, GSSAPI, NTLM, OAUTH2, SSPI from .version import Build, Version -__version__ = "4.7.2" +__version__ = "4.7.3" __all__ = [ "__version__", @@ -2932,38 +2932,29 @@

                      Inherited members

                      return session def create_oauth2_session(self): - has_token = False scope = ["https://outlook.office365.com/.default"] - session_params = {} + session_params = {"token": self.credentials.access_token} # Token may be None token_params = {} if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials): # Ask for a refresh token scope.append("offline_access") - # We don't know (or need) the Microsoft tenant ID. Use - # common/ to let Microsoft select the appropriate tenant - # for the provided authorization code or refresh token. + # We don't know (or need) the Microsoft tenant ID. Use common/ to let Microsoft select the appropriate + # tenant for the provided authorization code or refresh token. # # Suppress looks-like-password warning from Bandit. token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token" # nosec client_params = {} - has_token = self.credentials.access_token is not None - if has_token: - session_params["token"] = self.credentials.access_token - elif self.credentials.authorization_code is not None: - token_params["code"] = self.credentials.authorization_code - self.credentials.authorization_code = None + token_params["code"] = self.credentials.authorization_code # Auth code may be None + self.credentials.authorization_code = None # We can only use the code once if self.credentials.client_id is not None and self.credentials.client_secret is not None: - # If we're given a client ID and secret, we have enough - # to refresh access tokens ourselves. In other cases the - # session will raise TokenExpiredError and we'll need to - # ask the calling application to refresh the token (that - # covers cases where the caller doesn't have access to - # the client secret but is working with a service that - # can provide it refreshed tokens on a limited basis). + # If we're given a client ID and secret, we have enough to refresh access tokens ourselves. In other + # cases the session will raise TokenExpiredError, and we'll need to ask the calling application to + # refresh the token (that covers cases where the caller doesn't have access to the client secret but + # is working with a service that can provide it refreshed tokens on a limited basis). session_params.update( { "auto_refresh_kwargs": { @@ -2980,7 +2971,7 @@

                      Inherited members

                      client = BackendApplicationClient(client_id=self.credentials.client_id) session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params) - if not has_token: + if not session.token: # Fetch the token explicitly -- it doesn't occur implicitly token = session.fetch_token( token_url=token_url, @@ -2990,8 +2981,8 @@

                      Inherited members

                      timeout=self.TIMEOUT, **token_params, ) - # Allow the credentials object to update its copy of the new - # token, and give the application an opportunity to cache it + # Allow the credentials object to update its copy of the new token, and give the application an opportunity + # to cache it. self.credentials.on_token_auto_refreshed(token) session.auth = get_auth_instance(auth_type=OAUTH2, client=client) @@ -3237,38 +3228,29 @@

                      Methods

                      Expand source code
                      def create_oauth2_session(self):
                      -    has_token = False
                           scope = ["https://outlook.office365.com/.default"]
                      -    session_params = {}
                      +    session_params = {"token": self.credentials.access_token}  # Token may be None
                           token_params = {}
                       
                           if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials):
                               # Ask for a refresh token
                               scope.append("offline_access")
                       
                      -        # We don't know (or need) the Microsoft tenant ID. Use
                      -        # common/ to let Microsoft select the appropriate tenant
                      -        # for the provided authorization code or refresh token.
                      +        # We don't know (or need) the Microsoft tenant ID. Use common/ to let Microsoft select the appropriate
                      +        # tenant for the provided authorization code or refresh token.
                               #
                               # Suppress looks-like-password warning from Bandit.
                               token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"  # nosec
                       
                               client_params = {}
                      -        has_token = self.credentials.access_token is not None
                      -        if has_token:
                      -            session_params["token"] = self.credentials.access_token
                      -        elif self.credentials.authorization_code is not None:
                      -            token_params["code"] = self.credentials.authorization_code
                      -            self.credentials.authorization_code = None
                      +        token_params["code"] = self.credentials.authorization_code  # Auth code may be None
                      +        self.credentials.authorization_code = None  # We can only use the code once
                       
                               if self.credentials.client_id is not None and self.credentials.client_secret is not None:
                      -            # If we're given a client ID and secret, we have enough
                      -            # to refresh access tokens ourselves. In other cases the
                      -            # session will raise TokenExpiredError and we'll need to
                      -            # ask the calling application to refresh the token (that
                      -            # covers cases where the caller doesn't have access to
                      -            # the client secret but is working with a service that
                      -            # can provide it refreshed tokens on a limited basis).
                      +            # If we're given a client ID and secret, we have enough to refresh access tokens ourselves. In other
                      +            # cases the session will raise TokenExpiredError, and we'll need to ask the calling application to
                      +            # refresh the token (that covers cases where the caller doesn't have access to the client secret but
                      +            # is working with a service that can provide it refreshed tokens on a limited basis).
                                   session_params.update(
                                       {
                                           "auto_refresh_kwargs": {
                      @@ -3285,7 +3267,7 @@ 

                      Methods

                      client = BackendApplicationClient(client_id=self.credentials.client_id) session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params) - if not has_token: + if not session.token: # Fetch the token explicitly -- it doesn't occur implicitly token = session.fetch_token( token_url=token_url, @@ -3295,8 +3277,8 @@

                      Methods

                      timeout=self.TIMEOUT, **token_params, ) - # Allow the credentials object to update its copy of the new - # token, and give the application an opportunity to cache it + # Allow the credentials object to update its copy of the new token, and give the application an opportunity + # to cache it. self.credentials.on_token_auto_refreshed(token) session.auth = get_auth_instance(auth_type=OAUTH2, client=client) @@ -4668,12 +4650,12 @@

                      Inherited members

                      if auth_type is None: # Set a default auth type for the credentials where this makes sense auth_type = DEFAULT_AUTH_TYPE.get(type(credentials)) - elif credentials is None and auth_type in CREDENTIALS_REQUIRED: + if auth_type is not None and auth_type not in AUTH_TYPE_MAP: + raise InvalidEnumValue("auth_type", auth_type, AUTH_TYPE_MAP) + if credentials is None and auth_type in CREDENTIALS_REQUIRED: raise ValueError(f"Auth type {auth_type!r} was detected but no credentials were provided") if server and service_endpoint: raise AttributeError("Only one of 'server' or 'service_endpoint' must be provided") - if auth_type is not None and auth_type not in AUTH_TYPE_MAP: - raise InvalidEnumValue("auth_type", auth_type, AUTH_TYPE_MAP) if not retry_policy: retry_policy = FailFast() if not isinstance(version, (Version, type(None))): @@ -5918,7 +5900,7 @@

                      Methods

                      Ancestors

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

                      Class variables

                      @@ -7193,17 +7175,22 @@

                      Ancestors

                      Subclasses

                      • AllItems
                      • +
                      • ApplicationData
                      • Audits
                      • +
                      • Birthdays
                      • Calendar
                      • CalendarLogging
                      • CommonViews
                      • Contacts
                      • ConversationSettings
                      • +
                      • CrawlerData
                      • DefaultFoldersChangeHistory
                      • DeferredAction
                      • DeletedItems
                      • +
                      • DlpPolicyEvaluation
                      • ExchangeSyncData
                      • Files
                      • +
                      • FreeBusyCache
                      • FreebusyData
                      • GraphAnalytics
                      • Location
                      • @@ -7213,13 +7200,16 @@

                        Subclasses

                      • PassThroughSearchResults
                      • PdpProfileV2Secured
                      • RSSFeeds
                      • +
                      • RecoveryPoints
                      • Reminders
                      • Schedule
                      • Sharing
                      • Shortcuts
                      • Signal
                      • +
                      • SkypeTeamsMessages
                      • SmsAndChatsSync
                      • SpoolerQueue
                      • +
                      • SwssItems
                      • System
                      • System1
                      • Tasks
                      • @@ -8931,6 +8921,7 @@

                        Subclasses

                      • ConversationId
                      • FolderId
                      • MovedItemId
                      • +
                      • OldItemId
                      • ParentFolderId
                      • ParentItemId
                      • PersonaId
                      • @@ -9170,7 +9161,7 @@

                        Inherited members

                        conversation_topic = CharField(field_uri="message:ConversationTopic", is_read_only=True) # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword. author = MailboxField(field_uri="message:From", is_read_only_after_send=True) - message_id = CharField(field_uri="message:InternetMessageId", is_read_only_after_send=True) + message_id = TextField(field_uri="message:InternetMessageId", is_read_only_after_send=True) is_read = BooleanField(field_uri="message:IsRead", is_required=True, default=False) is_response_requested = BooleanField(field_uri="message:IsResponseRequested", default=False, is_required=True) references = TextField(field_uri="message:References") @@ -9302,7 +9293,7 @@

                        Inherited members

                        from ..services import MarkAsJunk res = MarkAsJunk(account=self.account).get( - items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item + items=[self], is_junk=is_junk, move_item=move_item, expect_result=None ) if res is None: return @@ -9476,7 +9467,7 @@

                        Methods

                        from ..services import MarkAsJunk res = MarkAsJunk(account=self.account).get( - items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item + items=[self], is_junk=is_junk, move_item=move_item, expect_result=None ) if res is None: return @@ -9672,15 +9663,15 @@

                        Methods

                        class OAuth2AuthorizationCodeCredentials -(authorization_code=None, access_token=None, **kwargs) +(authorization_code=None, access_token=None, client_id=None, client_secret=None, **kwargs)

                        Login info for OAuth 2.0 authentication using the authorization code grant type. This can be used in one of several ways: * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if supplied with a refresh token. -* Given an existing access token, refresh token, client ID, and client secret, use the access token until it -expires and then refresh it as needed. +* Given an existing access token, client ID, and client secret, use the access token until it expires and then +refresh it as needed. * Given only an existing access token, use it until it expires. This can be used to let the calling application refresh tokens itself by subclassing and implementing refresh().

                        Unlike the base (client credentials) grant, authorization code credentials don't require a Microsoft tenant ID @@ -9703,8 +9694,8 @@

                        Methods

                        several ways: * Given an authorization code, client ID, and client secret, fetch a token ourselves and refresh it as needed if supplied with a refresh token. - * Given an existing access token, refresh token, client ID, and client secret, use the access token until it - expires and then refresh it as needed. + * Given an existing access token, client ID, and client secret, use the access token until it expires and then + refresh it as needed. * Given only an existing access token, use it until it expires. This can be used to let the calling application refresh tokens itself by subclassing and implementing refresh(). @@ -9713,7 +9704,7 @@

                        Methods

                        tenant. """ - def __init__(self, authorization_code=None, access_token=None, **kwargs): + def __init__(self, authorization_code=None, access_token=None, client_id=None, client_secret=None, **kwargs): """ :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing @@ -9725,7 +9716,7 @@

                        Methods

                        :param access_token: Previously-obtained access token. If a token exists and the application will handle refreshing by itself (or opts not to handle it), this parameter alone is sufficient. """ - super().__init__(**kwargs) + super().__init__(client_id=client_id, client_secret=client_secret, **kwargs) self.authorization_code = authorization_code if access_token is not None and not isinstance(access_token, dict): raise InvalidTypeError("access_token", access_token, OAuth2Token) @@ -9763,7 +9754,7 @@

                        Inherited members

                        class OAuth2Credentials -(client_id, client_secret, tenant_id=None, identity=None) +(client_id, client_secret, tenant_id=None, identity=None, access_token=None)

                        Login info for OAuth 2.0 client credentials authentication, as well as a base for other OAuth 2.0 grant types.

                        @@ -9774,7 +9765,8 @@

                        Inherited members

                        :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing :param client_secret: Secret associated with the OAuth application :param tenant_id: Microsoft tenant ID of the account to access -:param identity: An Identity object representing the account that these credentials are connected to.

                        +:param identity: An Identity object representing the account that these credentials are connected to. +:param access_token: Previously-obtained access token, as a dict or an oauthlib.oauth2.OAuth2Token

                        Expand source code @@ -9788,21 +9780,21 @@

                        Inherited members

                        the associated auth code grant type for multi-tenant applications. """ - def __init__(self, client_id, client_secret, tenant_id=None, identity=None): + def __init__(self, client_id, client_secret, tenant_id=None, identity=None, access_token=None): """ :param client_id: ID of an authorized OAuth application, required for automatic token fetching and refreshing :param client_secret: Secret associated with the OAuth application :param tenant_id: Microsoft tenant ID of the account to access :param identity: An Identity object representing the account that these credentials are connected to. + :param access_token: Previously-obtained access token, as a dict or an oauthlib.oauth2.OAuth2Token """ super().__init__() self.client_id = client_id self.client_secret = client_secret self.tenant_id = tenant_id self.identity = identity - # When set, access_token is a dict (or an oauthlib.oauth2.OAuth2Token, which is also a dict) - self.access_token = None + self.access_token = access_token def refresh(self, session): # Creating a new session gets a new access token, so there's no work here to refresh the credentials. This @@ -11597,7 +11589,7 @@

                        Inherited members

                        :param folder_name: :param locale: a string, e.g. 'da_DK' """ - for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS: + for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS + MISC_FOLDERS: if folder_name.lower() in folder_cls.localized_names(locale): return folder_cls raise KeyError() @@ -11663,7 +11655,7 @@

                        Static methods

                        :param folder_name: :param locale: a string, e.g. 'da_DK' """ - for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS: + for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS + MISC_FOLDERS: if folder_name.lower() in folder_cls.localized_names(locale): return folder_cls raise KeyError()
                      diff --git a/docs/exchangelib/items/contact.html b/docs/exchangelib/items/contact.html index cd576c2f..a479a399 100644 --- a/docs/exchangelib/items/contact.html +++ b/docs/exchangelib/items/contact.html @@ -809,7 +809,9 @@

                      Inherited members

                      hobbies = StringAttributedValueField(field_uri="persona:Hobbies") wedding_anniversaries = StringAttributedValueField(field_uri="persona:WeddingAnniversaries") birthdays = StringAttributedValueField(field_uri="persona:Birthdays") - locations = StringAttributedValueField(field_uri="persona:Locations") + locations = StringAttributedValueField(field_uri="persona:Locations") + # This class has an additional field of type "ExtendedPropertyAttributedValueField" and + # field_uri 'persona:ExtendedProperties'

                      Ancestors

                        diff --git a/docs/exchangelib/items/index.html b/docs/exchangelib/items/index.html index ca08feaa..b7463e18 100644 --- a/docs/exchangelib/items/index.html +++ b/docs/exchangelib/items/index.html @@ -3015,7 +3015,7 @@

                        Inherited members

                        conversation_topic = CharField(field_uri="message:ConversationTopic", is_read_only=True) # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword. author = MailboxField(field_uri="message:From", is_read_only_after_send=True) - message_id = CharField(field_uri="message:InternetMessageId", is_read_only_after_send=True) + message_id = TextField(field_uri="message:InternetMessageId", is_read_only_after_send=True) is_read = BooleanField(field_uri="message:IsRead", is_required=True, default=False) is_response_requested = BooleanField(field_uri="message:IsResponseRequested", default=False, is_required=True) references = TextField(field_uri="message:References") @@ -3147,7 +3147,7 @@

                        Inherited members

                        from ..services import MarkAsJunk res = MarkAsJunk(account=self.account).get( - items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item + items=[self], is_junk=is_junk, move_item=move_item, expect_result=None ) if res is None: return @@ -3321,7 +3321,7 @@

                        Methods

                        from ..services import MarkAsJunk res = MarkAsJunk(account=self.account).get( - items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item + items=[self], is_junk=is_junk, move_item=move_item, expect_result=None ) if res is None: return @@ -3576,7 +3576,9 @@

                        Inherited members

                        hobbies = StringAttributedValueField(field_uri="persona:Hobbies") wedding_anniversaries = StringAttributedValueField(field_uri="persona:WeddingAnniversaries") birthdays = StringAttributedValueField(field_uri="persona:Birthdays") - locations = StringAttributedValueField(field_uri="persona:Locations") + locations = StringAttributedValueField(field_uri="persona:Locations") + # This class has an additional field of type "ExtendedPropertyAttributedValueField" and + # field_uri 'persona:ExtendedProperties'

                        Ancestors

                          diff --git a/docs/exchangelib/items/message.html b/docs/exchangelib/items/message.html index a5aafdb6..e724dee9 100644 --- a/docs/exchangelib/items/message.html +++ b/docs/exchangelib/items/message.html @@ -65,7 +65,7 @@

                          Module exchangelib.items.message

                          conversation_topic = CharField(field_uri="message:ConversationTopic", is_read_only=True) # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword. author = MailboxField(field_uri="message:From", is_read_only_after_send=True) - message_id = CharField(field_uri="message:InternetMessageId", is_read_only_after_send=True) + message_id = TextField(field_uri="message:InternetMessageId", is_read_only_after_send=True) is_read = BooleanField(field_uri="message:IsRead", is_required=True, default=False) is_response_requested = BooleanField(field_uri="message:IsResponseRequested", default=False, is_required=True) references = TextField(field_uri="message:References") @@ -197,7 +197,7 @@

                          Module exchangelib.items.message

                          from ..services import MarkAsJunk res = MarkAsJunk(account=self.account).get( - items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item + items=[self], is_junk=is_junk, move_item=move_item, expect_result=None ) if res is None: return @@ -316,7 +316,7 @@

                          Inherited members

                          conversation_topic = CharField(field_uri="message:ConversationTopic", is_read_only=True) # Rename 'From' to 'author'. We can't use fieldname 'from' since it's a Python keyword. author = MailboxField(field_uri="message:From", is_read_only_after_send=True) - message_id = CharField(field_uri="message:InternetMessageId", is_read_only_after_send=True) + message_id = TextField(field_uri="message:InternetMessageId", is_read_only_after_send=True) is_read = BooleanField(field_uri="message:IsRead", is_required=True, default=False) is_response_requested = BooleanField(field_uri="message:IsResponseRequested", default=False, is_required=True) references = TextField(field_uri="message:References") @@ -448,7 +448,7 @@

                          Inherited members

                          from ..services import MarkAsJunk res = MarkAsJunk(account=self.account).get( - items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item + items=[self], is_junk=is_junk, move_item=move_item, expect_result=None ) if res is None: return @@ -622,7 +622,7 @@

                          Methods

                          from ..services import MarkAsJunk res = MarkAsJunk(account=self.account).get( - items=[self], is_junk=is_junk, move_item=move_item, expect_result=move_item + items=[self], is_junk=is_junk, move_item=move_item, expect_result=None ) if res is None: return diff --git a/docs/exchangelib/properties.html b/docs/exchangelib/properties.html index 8684a767..68480f6d 100644 --- a/docs/exchangelib/properties.html +++ b/docs/exchangelib/properties.html @@ -35,7 +35,7 @@

                          Module exchangelib.properties

                          from inspect import getmro from threading import Lock -from .errors import InvalidTypeError, TimezoneDefinitionInvalidForYear +from .errors import InvalidTypeError from .fields import ( WEEKDAY_NAMES, AssociatedCalendarItemIdField, @@ -247,15 +247,15 @@

                          Module exchangelib.properties

                          # Folder class, making the custom field available for subclasses). if local_fields: kwargs["FIELDS"] = fields - cls = super().__new__(mcs, name, bases, kwargs) - cls._slots_keys = mcs._get_slots_keys(cls) - return cls + klass = super().__new__(mcs, name, bases, kwargs) + klass._slots_keys = mcs._get_slots_keys(klass) + return klass @staticmethod - def _get_slots_keys(cls): + def _get_slots_keys(klass): seen = set() keys = [] - for c in reversed(getmro(cls)): + for c in reversed(getmro(klass)): if not hasattr(c, "__slots__"): continue for k in c.__slots__: @@ -610,6 +610,24 @@

                          Module exchangelib.properties

                          return item.id, item.changekey +class OldItemId(ItemId): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/oldfolderid""" + + ELEMENT_NAME = "OldItemId" + + +class OldFolderId(FolderId): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/olditemid""" + + ELEMENT_NAME = "OldFolderId" + + +class OldParentFolderId(ParentFolderId): + """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/oldparentfolderid""" + + ELEMENT_NAME = "OldParentFolderId" + + class Mailbox(EWSElement): """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/mailbox""" @@ -1733,9 +1751,9 @@

                          Module exchangelib.properties

                          ITEM = "item" timestamp = DateTimeField(field_uri="TimeStamp") - item_id = EWSElementField(field_uri="ItemId", value_cls=ItemId) - folder_id = EWSElementField(field_uri="FolderId", value_cls=FolderId) - parent_folder_id = EWSElementField(field_uri="ParentFolderId", value_cls=ParentFolderId) + item_id = EWSElementField(value_cls=ItemId) + folder_id = EWSElementField(value_cls=FolderId) + parent_folder_id = EWSElementField(value_cls=ParentFolderId) @property def event_type(self): @@ -1749,9 +1767,9 @@

                          Module exchangelib.properties

                          class OldTimestampEvent(TimestampEvent, metaclass=EWSMeta): """Base class for both item and folder copy/move events.""" - old_item_id = EWSElementField(field_uri="OldItemId", value_cls=ItemId) - old_folder_id = EWSElementField(field_uri="OldFolderId", value_cls=FolderId) - old_parent_folder_id = EWSElementField(field_uri="OldParentFolderId", value_cls=ParentFolderId) + old_item_id = EWSElementField(value_cls=OldItemId) + old_folder_id = EWSElementField(value_cls=OldFolderId) + old_parent_folder_id = EWSElementField(value_cls=OldParentFolderId) class CopiedEvent(OldTimestampEvent): @@ -1892,18 +1910,6 @@

                          Module exchangelib.properties

                          name = CharField(field_uri="Name", is_attribute=True) bias = TimeDeltaField(field_uri="Bias", is_attribute=True) - def _split_id(self): - to_year, to_type = self.id.rsplit("/", 1)[1].split("-") - return int(to_year), to_type - - @property - def year(self): - return self._split_id()[0] - - @property - def type(self): - return self._split_id()[1] - @property def bias_in_minutes(self): return int(self.bias.total_seconds()) // 60 # Convert to minutes @@ -1934,18 +1940,15 @@

                          Module exchangelib.properties

                          def from_xml(cls, elem, account): return super().from_xml(elem, account) - def _get_standard_period(self, for_year): - # Look through periods and pick a relevant period according to the 'for_year' value - valid_period = None - for period in sorted(self.periods, key=lambda p: (p.year, p.type)): - if period.year > for_year: - break - if period.type != "Standard": + def _get_standard_period(self, transitions_group): + # Find the first standard period referenced from transitions_group + standard_periods_map = {p.id: p for p in self.periods if p.name == "Standard"} + for transition in transitions_group.transitions: + try: + return standard_periods_map[transition.to] + except KeyError: continue - valid_period = period - if valid_period is None: - raise TimezoneDefinitionInvalidForYear(f"Year {for_year} not included in periods {self.periods}") - return valid_period + raise ValueError(f"No standard period matching any transition in {transitions_group}") def _get_transitions_group(self, for_year): # Look through the transitions, and pick the relevant transition group according to the 'for_year' value @@ -1967,7 +1970,7 @@

                          Module exchangelib.properties

                          if not 0 <= len(transitions_group.transitions) <= 2: raise ValueError(f"Expected 0-2 transitions in transitions group {transitions_group}") - standard_period = self._get_standard_period(for_year) + standard_period = self._get_standard_period(transitions_group) periods_map = {p.id: p for p in self.periods} standard_time, daylight_time = None, None if len(transitions_group.transitions) == 1: @@ -3535,7 +3538,9 @@

                          Inherited members

                          class ConversationId(ItemId):
                               """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/conversationid"""
                           
                          -    ELEMENT_NAME = "ConversationId"
                          + ELEMENT_NAME = "ConversationId" + + # ChangeKey attribute is sometimes required, see MSDN link

                          Ancestors

                            @@ -4615,9 +4620,8 @@

                            Methods

                            (*args, **kwargs)
  • -

    type(object_or_name, bases, dict) -type(object) -> the object's type -type(name, bases, dict) -> a new type

    +

    type(object) -> the object's type +type(name, bases, dict, **kwds) -> a new type

    Expand source code @@ -4653,15 +4657,15 @@

    Methods

    # Folder class, making the custom field available for subclasses). if local_fields: kwargs["FIELDS"] = fields - cls = super().__new__(mcs, name, bases, kwargs) - cls._slots_keys = mcs._get_slots_keys(cls) - return cls + klass = super().__new__(mcs, name, bases, kwargs) + klass._slots_keys = mcs._get_slots_keys(klass) + return klass @staticmethod - def _get_slots_keys(cls): + def _get_slots_keys(klass): seen = set() keys = [] - for c in reversed(getmro(cls)): + for c in reversed(getmro(klass)): if not hasattr(c, "__slots__"): continue for k in c.__slots__: @@ -5435,6 +5439,7 @@

    Ancestors

    Subclasses

    Class variables

    @@ -6023,6 +6028,7 @@

    Subclasses

  • ConversationId
  • FolderId
  • MovedItemId
  • +
  • OldItemId
  • ParentFolderId
  • ParentItemId
  • PersonaId
  • @@ -6859,6 +6865,128 @@

    Inherited members

    +
    +class OldFolderId +(*args, **kwargs) +
    +
    + +
    + +Expand source code + +
    class OldFolderId(FolderId):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/olditemid"""
    +
    +    ELEMENT_NAME = "OldFolderId"
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class OldItemId +(*args, **kwargs) +
    +
    + +
    + +Expand source code + +
    class OldItemId(ItemId):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/oldfolderid"""
    +
    +    ELEMENT_NAME = "OldItemId"
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    +

    Inherited members

    + +
    +
    +class OldParentFolderId +(*args, **kwargs) +
    +
    + +
    + +Expand source code + +
    class OldParentFolderId(ParentFolderId):
    +    """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/oldparentfolderid"""
    +
    +    ELEMENT_NAME = "OldParentFolderId"
    +
    +

    Ancestors

    + +

    Class variables

    +
    +
    var ELEMENT_NAME
    +
    +
    +
    +
    +

    Inherited members

    + +
    class OldTimestampEvent (**kwargs) @@ -6872,9 +7000,9 @@

    Inherited members

    class OldTimestampEvent(TimestampEvent, metaclass=EWSMeta):
         """Base class for both item and folder copy/move events."""
     
    -    old_item_id = EWSElementField(field_uri="OldItemId", value_cls=ItemId)
    -    old_folder_id = EWSElementField(field_uri="OldFolderId", value_cls=FolderId)
    -    old_parent_folder_id = EWSElementField(field_uri="OldParentFolderId", value_cls=ParentFolderId)
    + old_item_id = EWSElementField(value_cls=OldItemId) + old_folder_id = EWSElementField(value_cls=OldFolderId) + old_parent_folder_id = EWSElementField(value_cls=OldParentFolderId)

    Ancestors

    +

    Subclasses

    +

    Class variables

    var ELEMENT_NAME
    @@ -7149,18 +7281,6 @@

    Inherited members

    name = CharField(field_uri="Name", is_attribute=True) bias = TimeDeltaField(field_uri="Bias", is_attribute=True) - def _split_id(self): - to_year, to_type = self.id.rsplit("/", 1)[1].split("-") - return int(to_year), to_type - - @property - def year(self): - return self._split_id()[0] - - @property - def type(self): - return self._split_id()[1] - @property def bias_in_minutes(self): return int(self.bias.total_seconds()) // 60 # Convert to minutes
    @@ -7206,30 +7326,6 @@

    Instance variables

    -
    var type
    -
    -
    -
    - -Expand source code - -
    @property
    -def type(self):
    -    return self._split_id()[1]
    -
    -
    -
    var year
    -
    -
    -
    - -Expand source code - -
    @property
    -def year(self):
    -    return self._split_id()[0]
    -
    -

    Inherited members

      @@ -9236,18 +9332,15 @@

      Inherited members

      def from_xml(cls, elem, account): return super().from_xml(elem, account) - def _get_standard_period(self, for_year): - # Look through periods and pick a relevant period according to the 'for_year' value - valid_period = None - for period in sorted(self.periods, key=lambda p: (p.year, p.type)): - if period.year > for_year: - break - if period.type != "Standard": + def _get_standard_period(self, transitions_group): + # Find the first standard period referenced from transitions_group + standard_periods_map = {p.id: p for p in self.periods if p.name == "Standard"} + for transition in transitions_group.transitions: + try: + return standard_periods_map[transition.to] + except KeyError: continue - valid_period = period - if valid_period is None: - raise TimezoneDefinitionInvalidForYear(f"Year {for_year} not included in periods {self.periods}") - return valid_period + raise ValueError(f"No standard period matching any transition in {transitions_group}") def _get_transitions_group(self, for_year): # Look through the transitions, and pick the relevant transition group according to the 'for_year' value @@ -9269,7 +9362,7 @@

      Inherited members

      if not 0 <= len(transitions_group.transitions) <= 2: raise ValueError(f"Expected 0-2 transitions in transitions group {transitions_group}") - standard_period = self._get_standard_period(for_year) + standard_period = self._get_standard_period(transitions_group) periods_map = {p.id: p for p in self.periods} standard_time, daylight_time = None, None if len(transitions_group.transitions) == 1: @@ -9373,7 +9466,7 @@

      Methods

      if not 0 <= len(transitions_group.transitions) <= 2: raise ValueError(f"Expected 0-2 transitions in transitions group {transitions_group}") - standard_period = self._get_standard_period(for_year) + standard_period = self._get_standard_period(transitions_group) periods_map = {p.id: p for p in self.periods} standard_time, daylight_time = None, None if len(transitions_group.transitions) == 1: @@ -9563,9 +9656,9 @@

      Inherited members

      ITEM = "item" timestamp = DateTimeField(field_uri="TimeStamp") - item_id = EWSElementField(field_uri="ItemId", value_cls=ItemId) - folder_id = EWSElementField(field_uri="FolderId", value_cls=FolderId) - parent_folder_id = EWSElementField(field_uri="ParentFolderId", value_cls=ParentFolderId) + item_id = EWSElementField(value_cls=ItemId) + folder_id = EWSElementField(value_cls=FolderId) + parent_folder_id = EWSElementField(value_cls=ParentFolderId) @property def event_type(self): @@ -10830,6 +10923,24 @@

      OldFolderId

      + + +
    • +

      OldItemId

      + +
    • +
    • +

      OldParentFolderId

      + +
    • +
    • OldTimestampEvent

    • diff --git a/docs/exchangelib/protocol.html b/docs/exchangelib/protocol.html index bde37eaa..ce1a0410 100644 --- a/docs/exchangelib/protocol.html +++ b/docs/exchangelib/protocol.html @@ -340,38 +340,29 @@

      Module exchangelib.protocol

      return session def create_oauth2_session(self): - has_token = False scope = ["https://outlook.office365.com/.default"] - session_params = {} + session_params = {"token": self.credentials.access_token} # Token may be None token_params = {} if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials): # Ask for a refresh token scope.append("offline_access") - # We don't know (or need) the Microsoft tenant ID. Use - # common/ to let Microsoft select the appropriate tenant - # for the provided authorization code or refresh token. + # We don't know (or need) the Microsoft tenant ID. Use common/ to let Microsoft select the appropriate + # tenant for the provided authorization code or refresh token. # # Suppress looks-like-password warning from Bandit. token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token" # nosec client_params = {} - has_token = self.credentials.access_token is not None - if has_token: - session_params["token"] = self.credentials.access_token - elif self.credentials.authorization_code is not None: - token_params["code"] = self.credentials.authorization_code - self.credentials.authorization_code = None + token_params["code"] = self.credentials.authorization_code # Auth code may be None + self.credentials.authorization_code = None # We can only use the code once if self.credentials.client_id is not None and self.credentials.client_secret is not None: - # If we're given a client ID and secret, we have enough - # to refresh access tokens ourselves. In other cases the - # session will raise TokenExpiredError and we'll need to - # ask the calling application to refresh the token (that - # covers cases where the caller doesn't have access to - # the client secret but is working with a service that - # can provide it refreshed tokens on a limited basis). + # If we're given a client ID and secret, we have enough to refresh access tokens ourselves. In other + # cases the session will raise TokenExpiredError, and we'll need to ask the calling application to + # refresh the token (that covers cases where the caller doesn't have access to the client secret but + # is working with a service that can provide it refreshed tokens on a limited basis). session_params.update( { "auto_refresh_kwargs": { @@ -388,7 +379,7 @@

      Module exchangelib.protocol

      client = BackendApplicationClient(client_id=self.credentials.client_id) session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params) - if not has_token: + if not session.token: # Fetch the token explicitly -- it doesn't occur implicitly token = session.fetch_token( token_url=token_url, @@ -398,8 +389,8 @@

      Module exchangelib.protocol

      timeout=self.TIMEOUT, **token_params, ) - # Allow the credentials object to update its copy of the new - # token, and give the application an opportunity to cache it + # Allow the credentials object to update its copy of the new token, and give the application an opportunity + # to cache it. self.credentials.on_token_auto_refreshed(token) session.auth = get_auth_instance(auth_type=OAUTH2, client=client) @@ -1139,38 +1130,29 @@

      Classes

      return session def create_oauth2_session(self): - has_token = False scope = ["https://outlook.office365.com/.default"] - session_params = {} + session_params = {"token": self.credentials.access_token} # Token may be None token_params = {} if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials): # Ask for a refresh token scope.append("offline_access") - # We don't know (or need) the Microsoft tenant ID. Use - # common/ to let Microsoft select the appropriate tenant - # for the provided authorization code or refresh token. + # We don't know (or need) the Microsoft tenant ID. Use common/ to let Microsoft select the appropriate + # tenant for the provided authorization code or refresh token. # # Suppress looks-like-password warning from Bandit. token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token" # nosec client_params = {} - has_token = self.credentials.access_token is not None - if has_token: - session_params["token"] = self.credentials.access_token - elif self.credentials.authorization_code is not None: - token_params["code"] = self.credentials.authorization_code - self.credentials.authorization_code = None + token_params["code"] = self.credentials.authorization_code # Auth code may be None + self.credentials.authorization_code = None # We can only use the code once if self.credentials.client_id is not None and self.credentials.client_secret is not None: - # If we're given a client ID and secret, we have enough - # to refresh access tokens ourselves. In other cases the - # session will raise TokenExpiredError and we'll need to - # ask the calling application to refresh the token (that - # covers cases where the caller doesn't have access to - # the client secret but is working with a service that - # can provide it refreshed tokens on a limited basis). + # If we're given a client ID and secret, we have enough to refresh access tokens ourselves. In other + # cases the session will raise TokenExpiredError, and we'll need to ask the calling application to + # refresh the token (that covers cases where the caller doesn't have access to the client secret but + # is working with a service that can provide it refreshed tokens on a limited basis). session_params.update( { "auto_refresh_kwargs": { @@ -1187,7 +1169,7 @@

      Classes

      client = BackendApplicationClient(client_id=self.credentials.client_id) session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params) - if not has_token: + if not session.token: # Fetch the token explicitly -- it doesn't occur implicitly token = session.fetch_token( token_url=token_url, @@ -1197,8 +1179,8 @@

      Classes

      timeout=self.TIMEOUT, **token_params, ) - # Allow the credentials object to update its copy of the new - # token, and give the application an opportunity to cache it + # Allow the credentials object to update its copy of the new token, and give the application an opportunity + # to cache it. self.credentials.on_token_auto_refreshed(token) session.auth = get_auth_instance(auth_type=OAUTH2, client=client) @@ -1444,38 +1426,29 @@

      Methods

      Expand source code
      def create_oauth2_session(self):
      -    has_token = False
           scope = ["https://outlook.office365.com/.default"]
      -    session_params = {}
      +    session_params = {"token": self.credentials.access_token}  # Token may be None
           token_params = {}
       
           if isinstance(self.credentials, OAuth2AuthorizationCodeCredentials):
               # Ask for a refresh token
               scope.append("offline_access")
       
      -        # We don't know (or need) the Microsoft tenant ID. Use
      -        # common/ to let Microsoft select the appropriate tenant
      -        # for the provided authorization code or refresh token.
      +        # We don't know (or need) the Microsoft tenant ID. Use common/ to let Microsoft select the appropriate
      +        # tenant for the provided authorization code or refresh token.
               #
               # Suppress looks-like-password warning from Bandit.
               token_url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"  # nosec
       
               client_params = {}
      -        has_token = self.credentials.access_token is not None
      -        if has_token:
      -            session_params["token"] = self.credentials.access_token
      -        elif self.credentials.authorization_code is not None:
      -            token_params["code"] = self.credentials.authorization_code
      -            self.credentials.authorization_code = None
      +        token_params["code"] = self.credentials.authorization_code  # Auth code may be None
      +        self.credentials.authorization_code = None  # We can only use the code once
       
               if self.credentials.client_id is not None and self.credentials.client_secret is not None:
      -            # If we're given a client ID and secret, we have enough
      -            # to refresh access tokens ourselves. In other cases the
      -            # session will raise TokenExpiredError and we'll need to
      -            # ask the calling application to refresh the token (that
      -            # covers cases where the caller doesn't have access to
      -            # the client secret but is working with a service that
      -            # can provide it refreshed tokens on a limited basis).
      +            # If we're given a client ID and secret, we have enough to refresh access tokens ourselves. In other
      +            # cases the session will raise TokenExpiredError, and we'll need to ask the calling application to
      +            # refresh the token (that covers cases where the caller doesn't have access to the client secret but
      +            # is working with a service that can provide it refreshed tokens on a limited basis).
                   session_params.update(
                       {
                           "auto_refresh_kwargs": {
      @@ -1492,7 +1465,7 @@ 

      Methods

      client = BackendApplicationClient(client_id=self.credentials.client_id) session = self.raw_session(self.service_endpoint, oauth2_client=client, oauth2_session_params=session_params) - if not has_token: + if not session.token: # Fetch the token explicitly -- it doesn't occur implicitly token = session.fetch_token( token_url=token_url, @@ -1502,8 +1475,8 @@

      Methods

      timeout=self.TIMEOUT, **token_params, ) - # Allow the credentials object to update its copy of the new - # token, and give the application an opportunity to cache it + # Allow the credentials object to update its copy of the new token, and give the application an opportunity + # to cache it. self.credentials.on_token_auto_refreshed(token) session.auth = get_auth_instance(auth_type=OAUTH2, client=client) diff --git a/docs/exchangelib/services/common.html b/docs/exchangelib/services/common.html index a4ee88eb..866fef8e 100644 --- a/docs/exchangelib/services/common.html +++ b/docs/exchangelib/services/common.html @@ -28,55 +28,29 @@

      Module exchangelib.services.common

      import abc
       import logging
      -import traceback
       from itertools import chain
       
       from .. import errors
       from ..attachments import AttachmentId
       from ..credentials import IMPERSONATION, OAuth2Credentials
       from ..errors import (
      -    ErrorAccessDenied,
      -    ErrorADUnavailable,
           ErrorBatchProcessingStopped,
           ErrorCannotDeleteObject,
           ErrorCannotDeleteTaskOccurrence,
      -    ErrorCannotEmptyFolder,
      -    ErrorConnectionFailed,
      -    ErrorConnectionFailedTransientError,
           ErrorCorruptData,
      -    ErrorCreateItemAccessDenied,
      -    ErrorDelegateNoUser,
      -    ErrorDeleteDistinguishedFolder,
           ErrorExceededConnectionCount,
      -    ErrorFolderNotFound,
      -    ErrorImpersonateUserDenied,
      -    ErrorImpersonationFailed,
           ErrorIncorrectSchemaVersion,
      -    ErrorInternalServerError,
      -    ErrorInternalServerTransientError,
           ErrorInvalidChangeKey,
           ErrorInvalidIdMalformed,
      -    ErrorInvalidLicense,
           ErrorInvalidRequest,
           ErrorInvalidSchemaVersionForMailboxVersion,
           ErrorInvalidServerVersion,
      -    ErrorInvalidSubscription,
      -    ErrorInvalidSyncStateData,
      -    ErrorInvalidWatermark,
           ErrorItemCorrupt,
           ErrorItemNotFound,
           ErrorItemSave,
      -    ErrorMailboxMoveInProgress,
      -    ErrorMailboxStoreUnavailable,
      +    ErrorMailRecipientNotFound,
           ErrorMessageSizeExceeded,
           ErrorMimeContentConversionFailed,
      -    ErrorNameResolutionMultipleResults,
      -    ErrorNameResolutionNoResults,
      -    ErrorNonExistentMailbox,
      -    ErrorNoPublicFolderReplicaAvailable,
      -    ErrorNoRespondingCASInDestinationSite,
      -    ErrorNotDelegate,
      -    ErrorQuotaExceeded,
           ErrorRecurrenceHasNoOccurrence,
           ErrorServerBusy,
           ErrorTimeoutExpired,
      @@ -84,11 +58,9 @@ 

      Module exchangelib.services.common

      EWSWarning, InvalidTypeError, MalformedResponseError, - RateLimitError, SessionPoolMinSizeReached, SOAPError, TransportError, - UnauthorizedError, ) from ..folders import BaseFolder, Folder, RootOfHierarchy from ..items import BaseItem @@ -126,44 +98,6 @@

      Module exchangelib.services.common

      PAGE_SIZE = 100 # A default page size for all paging services. This is the number of items we request per page CHUNK_SIZE = 100 # A default chunk size for all services. This is the number of items we send in a single request -KNOWN_EXCEPTIONS = ( - ErrorAccessDenied, - ErrorADUnavailable, - ErrorBatchProcessingStopped, - ErrorCannotDeleteObject, - ErrorCannotEmptyFolder, - ErrorConnectionFailed, - ErrorConnectionFailedTransientError, - ErrorCreateItemAccessDenied, - ErrorDelegateNoUser, - ErrorDeleteDistinguishedFolder, - ErrorExceededConnectionCount, - ErrorFolderNotFound, - ErrorImpersonateUserDenied, - ErrorImpersonationFailed, - ErrorInternalServerError, - ErrorInternalServerTransientError, - ErrorInvalidChangeKey, - ErrorInvalidLicense, - ErrorInvalidSubscription, - ErrorInvalidSyncStateData, - ErrorInvalidWatermark, - ErrorItemCorrupt, - ErrorItemNotFound, - ErrorMailboxMoveInProgress, - ErrorMailboxStoreUnavailable, - ErrorNameResolutionMultipleResults, - ErrorNameResolutionNoResults, - ErrorNonExistentMailbox, - ErrorNoPublicFolderReplicaAvailable, - ErrorNoRespondingCASInDestinationSite, - ErrorNotDelegate, - ErrorQuotaExceeded, - ErrorTimeoutExpired, - RateLimitError, - UnauthorizedError, -) - class EWSService(metaclass=abc.ABCMeta): """Base class for all EWS services.""" @@ -186,6 +120,7 @@

      Module exchangelib.services.common

      ErrorRecurrenceHasNoOccurrence, ErrorCorruptData, ErrorItemCorrupt, + ErrorMailRecipientNotFound, ) # Similarly, define the warnings we want to return unraised WARNINGS_TO_CATCH_IN_RESPONSE = ErrorBatchProcessingStopped @@ -360,9 +295,6 @@

      Module exchangelib.services.common

      except ErrorServerBusy as e: self._handle_backoff(e) continue - except KNOWN_EXCEPTIONS: - # These are known and understood, and don't require a backtrace. - raise except (ErrorTooManyObjectsOpened, ErrorTimeoutExpired) as e: # ErrorTooManyObjectsOpened means there are too many connections to the Exchange database. This is very # often a symptom of sending too many requests. @@ -377,11 +309,6 @@

      Module exchangelib.services.common

      # Re-raise as an ErrorServerBusy with a default delay of 5 minutes raise ErrorServerBusy(f"Reraised from {e.__class__.__name__}({e})") - except Exception: - # This may run in a thread, which obfuscates the stack trace. Print trace immediately. - account = self.account if isinstance(self, EWSAccountService) else None - log.warning("Account %s: Exception in _get_elements: %s", account, traceback.format_exc(20)) - raise finally: if self.streaming: self.stop_streaming() @@ -809,7 +736,7 @@

      Module exchangelib.services.common

      def _account_to_impersonate(self): if self.account.access_type == IMPERSONATION: return self.account.identity - return None + return super()._account_to_impersonate @property def _timezone(self): @@ -1232,7 +1159,7 @@

      Classes

      def _account_to_impersonate(self): if self.account.access_type == IMPERSONATION: return self.account.identity - return None + return super()._account_to_impersonate @property def _timezone(self): @@ -1504,6 +1431,7 @@

      Inherited members

      ErrorRecurrenceHasNoOccurrence, ErrorCorruptData, ErrorItemCorrupt, + ErrorMailRecipientNotFound, ) # Similarly, define the warnings we want to return unraised WARNINGS_TO_CATCH_IN_RESPONSE = ErrorBatchProcessingStopped @@ -1678,9 +1606,6 @@

      Inherited members

      except ErrorServerBusy as e: self._handle_backoff(e) continue - except KNOWN_EXCEPTIONS: - # These are known and understood, and don't require a backtrace. - raise except (ErrorTooManyObjectsOpened, ErrorTimeoutExpired) as e: # ErrorTooManyObjectsOpened means there are too many connections to the Exchange database. This is very # often a symptom of sending too many requests. @@ -1695,11 +1620,6 @@

      Inherited members

      # Re-raise as an ErrorServerBusy with a default delay of 5 minutes raise ErrorServerBusy(f"Reraised from {e.__class__.__name__}({e})") - except Exception: - # This may run in a thread, which obfuscates the stack trace. Print trace immediately. - account = self.account if isinstance(self, EWSAccountService) else None - log.warning("Account %s: Exception in _get_elements: %s", account, traceback.format_exc(20)) - raise finally: if self.streaming: self.stop_streaming() diff --git a/docs/exchangelib/services/get_user_availability.html b/docs/exchangelib/services/get_user_availability.html index ba93a061..6e1a0d84 100644 --- a/docs/exchangelib/services/get_user_availability.html +++ b/docs/exchangelib/services/get_user_availability.html @@ -71,9 +71,11 @@

      Module exchangelib.services.get_user_availability def _get_elements_in_response(self, response): for msg in response: - # Just check the response code and raise errors - self._get_element_container(message=msg.find(f"{{{MNS}}}ResponseMessage")) - yield from self._get_elements_in_container(container=msg) + container_or_exc = self._get_element_container(message=msg.find(f"{{{MNS}}}ResponseMessage")) + if isinstance(container_or_exc, Exception): + yield container_or_exc + else: + yield from self._get_elements_in_container(container=msg) @classmethod def _get_elements_in_container(cls, container): @@ -141,9 +143,11 @@

      Classes

      def _get_elements_in_response(self, response): for msg in response: - # Just check the response code and raise errors - self._get_element_container(message=msg.find(f"{{{MNS}}}ResponseMessage")) - yield from self._get_elements_in_container(container=msg) + container_or_exc = self._get_element_container(message=msg.find(f"{{{MNS}}}ResponseMessage")) + if isinstance(container_or_exc, Exception): + yield container_or_exc + else: + yield from self._get_elements_in_container(container=msg) @classmethod def _get_elements_in_container(cls, container): diff --git a/docs/exchangelib/services/index.html b/docs/exchangelib/services/index.html index b10f1196..dac091e4 100644 --- a/docs/exchangelib/services/index.html +++ b/docs/exchangelib/services/index.html @@ -4225,9 +4225,11 @@

      Inherited members

      def _get_elements_in_response(self, response): for msg in response: - # Just check the response code and raise errors - self._get_element_container(message=msg.find(f"{{{MNS}}}ResponseMessage")) - yield from self._get_elements_in_container(container=msg) + container_or_exc = self._get_element_container(message=msg.find(f"{{{MNS}}}ResponseMessage")) + if isinstance(container_or_exc, Exception): + yield container_or_exc + else: + yield from self._get_elements_in_container(container=msg) @classmethod def _get_elements_in_container(cls, container):