exchangelib.folders.roots API documentation

# Module `exchangelib.folders.roots`

## Classes

`class ArchiveRoot (**kwargs)`
  Expand source code

      class ArchiveRoot(RootOfHierarchy):
          """The root of the archive folders hierarchy. Not available on all mailboxes."""

          DISTINGUISHED_FOLDER_ID = "archiveroot"
          supported_from = EXCHANGE_2010_SP1
          WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ARCHIVE_ROOT

  The root of the archive folders hierarchy. Not available on all mailboxes.

  ### Ancestors

  - RootOfHierarchy
  - BaseFolder
  - RegisterMixIn
  - IdChangeKeyMixIn
  - EWSElement
  - SearchableMixIn
  - SupportedVersionClassMixIn

  ### Class variables

  `var DISTINGUISHED_FOLDER_ID`
  `var WELLKNOWN_FOLDERS`
  `var supported_from`

  ### Inherited members

  - `RootOfHierarchy`:
    - `ID_ELEMENT_CLS`
    - `account`
    - `add_field`
    - `all`
    - `deregister`
    - `exclude`
    - `filter`
    - `folder_cls_from_container_class`
    - `folder_cls_from_folder_name`
    - `get`
    - `get_default_folder`
    - `get_distinguished`
    - `get_events`
    - `get_streaming_events`
    - `none`
    - `parent`
    - `people`
    - `register`
    - `remove_field`
    - `root`
    - `subscribe_to_pull`
    - `subscribe_to_push`
    - `subscribe_to_streaming`
    - `supported_fields`
    - `sync_hierarchy`
    - `sync_items`
    - `test_access`
    - `tree`
    - `unsubscribe`
    - `validate_field`
`class PublicFoldersRoot (**kwargs)`
  Expand source code

      class PublicFoldersRoot(RootOfHierarchy):
          """The root of the public folder hierarchy. Not available on all mailboxes."""

          DISTINGUISHED_FOLDER_ID = "publicfoldersroot"
          DEFAULT_FOLDER_TRAVERSAL_DEPTH = SHALLOW
          supported_from = EXCHANGE_2007_SP1

          def __init__(self, **kwargs):
              super().__init__(**kwargs)
              if self._distinguished_id:
                  self._distinguished_id.mailbox = None  # See DistinguishedFolderId.clean()

          @property
          def _folders_map(self):
              # Top-level public folders may point to the root folder of the owning account and not the public folders root
              # of this account. This breaks the assumption of get_children(). Fix it by overwriting the parent folder.
              fix_parents = self._subfolders is None
              res = super()._folders_map
              if fix_parents:
                  with self._subfolders_lock:
                      for f in res.values():
                          if f.id != self.id:
                              f.parent = self
              return res

          def get_children(self, folder):
              # EWS does not allow deep traversal of public folders, so self._folders_map will only populate the top-level
              # subfolders. To traverse public folders at arbitrary depth, we need to get child folders on demand.

              # Let's check if this folder already has any cached children. If so, assume we can just return those.
              children = list(super().get_children(folder=folder))
              if children:
                  # Return a generator like our parent does
                  yield from children
                  return

              # Also return early if the server told us that there are no child folders.
              if folder.child_folder_count == 0:
                  return

              children_map = {}
              with suppress(ErrorAccessDenied):
                  for f in (
                      SingleFolderQuerySet(account=self.account, folder=folder)
                      .depth(self.DEFAULT_FOLDER_TRAVERSAL_DEPTH)
                      .all()
                  ):
                      if isinstance(f, MISSING_FOLDER_ERRORS):
                          # We were unlucky. The folder disappeared between the FindFolder and the GetFolder calls
                          continue
                      if isinstance(f, Exception):
                          raise f
                      children_map[f.id] = f

              # Let's update the cache atomically, to avoid partial reads of the cache.
              with self._subfolders_lock:
                  self._subfolders.update(children_map)

              # Child folders have been cached now. Try super().get_children() again.
              yield from super().get_children(folder=folder)

  The root of the public folder hierarchy. Not available on all mailboxes.

  ### Ancestors

  - RootOfHierarchy
  - BaseFolder
  - RegisterMixIn
  - IdChangeKeyMixIn
  - EWSElement
  - SearchableMixIn
  - SupportedVersionClassMixIn

  ### Class variables

  `var DEFAULT_FOLDER_TRAVERSAL_DEPTH`
  `var DISTINGUISHED_FOLDER_ID`
  `var supported_from`

  ### Methods

  `def get_children(self, folder)`
    Expand source code

        def get_children(self, folder):
            # EWS does not allow deep traversal of public folders, so self._folders_map will only populate the top-level
            # subfolders. To traverse public folders at arbitrary depth, we need to get child folders on demand.

            # Let's check if this folder already has any cached children. If so, assume we can just return those.
            children = list(super().get_children(folder=folder))
            if children:
                # Return a generator like our parent does
                yield from children
                return

            # Also return early if the server told us that there are no child folders.
            if folder.child_folder_count == 0:
                return

            children_map = {}
            with suppress(ErrorAccessDenied):
                for f in (
                    SingleFolderQuerySet(account=self.account, folder=folder)
                    .depth(self.DEFAULT_FOLDER_TRAVERSAL_DEPTH)
                    .all()
                ):
                    if isinstance(f, MISSING_FOLDER_ERRORS):
                        # We were unlucky. The folder disappeared between the FindFolder and the GetFolder calls
                        continue
                    if isinstance(f, Exception):
                        raise f
                    children_map[f.id] = f

            # Let's update the cache atomically, to avoid partial reads of the cache.
            with self._subfolders_lock:
                self._subfolders.update(children_map)

            # Child folders have been cached now. Try super().get_children() again.
            yield from super().get_children(folder=folder)

  ### Inherited members

  - `RootOfHierarchy`:
    - `ID_ELEMENT_CLS`
    - `account`
    - `add_field`
    - `all`
    - `deregister`
    - `exclude`
    - `filter`
    - `folder_cls_from_container_class`
    - `folder_cls_from_folder_name`
    - `get`
    - `get_default_folder`
    - `get_distinguished`
    - `get_events`
    - `get_streaming_events`
    - `none`
    - `parent`
    - `people`
    - `register`
    - `remove_field`
    - `root`
    - `subscribe_to_pull`
    - `subscribe_to_push`
    - `subscribe_to_streaming`
    - `supported_fields`
    - `sync_hierarchy`
    - `sync_items`
    - `test_access`
    - `tree`
    - `unsubscribe`
    - `validate_field`
`class Root (**kwargs)`
  Expand source code

      class Root(RootOfHierarchy):
          """The root of the standard folder hierarchy."""

          DISTINGUISHED_FOLDER_ID = "root"
          WELLKNOWN_FOLDERS = WELLKNOWN_FOLDERS_IN_ROOT

          @property
          def tois(self):
              # 'Top of Information Store' is a folder available in some Exchange accounts. It usually contains the
              # distinguished folders belonging to the account (inbox, calendar, trash etc.).
              return self.get_default_folder(MsgFolderRoot)

          def get_default_folder(self, folder_cls):
              with suppress(MISSING_FOLDER_ERRORS):
                  return super().get_default_folder(folder_cls)

              # Try to pick a suitable default folder. we do this by:
              #  1. Searching the full folder list for a folder with the distinguished folder name
              #  2. Searching TOIS for a direct child folder of the same type that is marked as distinguished
              #  3. Searching TOIS for a direct child folder of the same type that has a localized name
              #  4. Searching root for a direct child folder of the same type that is marked as distinguished
              #  5. Searching root for a direct child folder of the same type that has a localized name
              log.debug("Searching default %s folder in full folder list", folder_cls)

              for f in self._folders_map.values():
                  # Require exact type, to avoid matching with subclasses (e.g. RecipientCache and Contacts)
                  if f.__class__ == folder_cls and f.has_distinguished_name:
                      log.debug("Found cached %s folder with default distinguished name", folder_cls)
                      return f

              # Try direct children of TOIS first, unless we're trying to get the TOIS folder
              if folder_cls != MsgFolderRoot:
                  with suppress(MISSING_FOLDER_ERRORS):
                      return self._get_candidate(folder_cls=folder_cls, folder_coll=self.tois.children)
                  # No candidates, or TOIS does not exist, or we don't have access to TOIS

              # Finally, try direct children of root
              return self._get_candidate(folder_cls=folder_cls, folder_coll=self.children)

          def _get_candidate(self, folder_cls, folder_coll):
              # Look for a single useful folder of type folder_cls in folder_coll
              same_type = [f for f in folder_coll if f.__class__ == folder_cls]
              are_distinguished = [f for f in same_type if f.is_distinguished]
              if are_distinguished:
                  candidates = are_distinguished
              else:
                  candidates = [f for f in same_type if f.name.lower() in folder_cls.localized_names(self.account.locale)]
              if not candidates:
                  raise ErrorFolderNotFound(f"No usable default {folder_cls} folders")
              if len(candidates) > 1:
                  raise ValueError(f"Multiple possible default {folder_cls} folders: {[f.name for f in candidates]}")
              candidate = candidates[0]
              if candidate.is_distinguished:
                  log.debug("Found distinguished %s folder", folder_cls)
              else:
                  log.debug("Found %s folder with localized name %s", folder_cls, candidate.name)
              return candidate

  The root of the standard folder hierarchy.

  ### Ancestors

  - RootOfHierarchy
  - BaseFolder
  - RegisterMixIn
  - IdChangeKeyMixIn
  - EWSElement
  - SearchableMixIn
  - SupportedVersionClassMixIn

  ### Class variables

  `var DISTINGUISHED_FOLDER_ID`
  `var WELLKNOWN_FOLDERS`

  ### Instance variables

  `prop tois`
    Expand source code

        @property
        def tois(self):
            # 'Top of Information Store' is a folder available in some Exchange accounts. It usually contains the
            # distinguished folders belonging to the account (inbox, calendar, trash etc.).
            return self.get_default_folder(MsgFolderRoot)

  ### Inherited members

  - `RootOfHierarchy`:
    - `ID_ELEMENT_CLS`
    - `account`
    - `add_field`
    - `all`
    - `deregister`
    - `exclude`
    - `filter`
    - `folder_cls_from_container_class`
    - `folder_cls_from_folder_name`
    - `get`
    - `get_default_folder`
    - `get_distinguished`
    - `get_events`
    - `get_streaming_events`
    - `none`
    - `parent`
    - `people`
    - `register`
    - `remove_field`
    - `root`
    - `subscribe_to_pull`
    - `subscribe_to_push`
    - `subscribe_to_streaming`
    - `supported_fields`
    - `sync_hierarchy`
    - `sync_items`
    - `test_access`
    - `tree`
    - `unsubscribe`
    - `validate_field`
`class RootOfHierarchy (**kwargs)`
  Expand source code

      class RootOfHierarchy(BaseFolder, metaclass=EWSMeta):
          """Base class for folders that implement the root of a folder hierarchy."""

          # A list of wellknown, or "distinguished", folders that are belong in this folder hierarchy. See
          # https://docs.microsoft.com/en-us/dotnet/api/microsoft.exchange.webservices.data.wellknownfoldername
          # and https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
          # 'RootOfHierarchy' subclasses must not be in this list.
          WELLKNOWN_FOLDERS = []

          # This folder type also has 'folder:PermissionSet' on some server versions, but requesting it sometimes causes
          # 'ErrorAccessDenied', as reported by some users. Ignore it entirely for root folders - it's usefulness is
          # deemed minimal at best.
          effective_rights = EffectiveRightsField(
              field_uri="folder:EffectiveRights", is_read_only=True, supported_from=EXCHANGE_2007_SP1
          )

          __slots__ = "_account", "_subfolders", "_subfolders_lock"

          # A special folder that acts as the top of a folder hierarchy. Finds and caches sub-folders at arbitrary depth.
          def __init__(self, **kwargs):
              self._account = kwargs.pop("account", None)  # A pointer back to the account holding the folder hierarchy
              super().__init__(**kwargs)
              self._subfolders = None  # See self._folders_map()
              self._subfolders_lock = Lock()

          @property
          def account(self):
              return self._account

          @property
          def root(self):
              return self

          @property
          def parent(self):
              return None

          @classmethod
          def register(cls, *args, **kwargs):
              if cls is not RootOfHierarchy:
                  raise TypeError("For folder roots, custom fields must be registered on the RootOfHierarchy class")
              return super().register(*args, **kwargs)

          @classmethod
          def deregister(cls, *args, **kwargs):
              if cls is not RootOfHierarchy:
                  raise TypeError("For folder roots, custom fields must be registered on the RootOfHierarchy class")
              return super().deregister(*args, **kwargs)

          def get_folder(self, folder):
              if not folder.id:
                  raise ValueError("'folder' must have an ID")
              return self._folders_map.get(folder.id)

          def add_folder(self, folder):
              if not folder.id:
                  raise ValueError("'folder' must have an ID")
              self._folders_map[folder.id] = folder

          def update_folder(self, folder):
              if not folder.id:
                  raise ValueError("'folder' must have an ID")
              self._folders_map[folder.id] = folder

          def remove_folder(self, folder):
              if not folder.id:
                  raise ValueError("'folder' must have an ID")
              with suppress(KeyError):
                  del self._folders_map[folder.id]

          def clear_cache(self):
              with self._subfolders_lock:
                  self._subfolders = None

          def get_children(self, folder):
              for f in self._folders_map.values():
                  if not f.parent:
                      continue
                  if f.parent.id == folder.id:
                      yield f

          def get_default_folder(self, folder_cls):
              """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
              folder was found, try as best we can to return the default folder of type 'folder_cls'
              """
              if not folder_cls.DISTINGUISHED_FOLDER_ID:
                  raise ValueError(f"'folder_cls' {folder_cls} must have a DISTINGUISHED_FOLDER_ID value")
              # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
              # for accessing e.g. 'account.contacts' without fetching all folders of the account.
              if self._subfolders is not None:
                  for f in self._folders_map.values():
                      # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts
                      if f.__class__ == folder_cls and f.is_distinguished:
                          log.debug("Found cached distinguished %s folder", folder_cls)
                          return f
              try:
                  log.debug("Requesting distinguished %s folder explicitly", folder_cls)
                  return folder_cls.get_distinguished(root=self)
              except ErrorAccessDenied:
                  # Maybe we just don't have GetFolder access? Try FindItem instead
                  log.debug("Testing default %s folder with FindItem", folder_cls)
                  fld = folder_cls(
                      _distinguished_id=DistinguishedFolderId(
                          id=folder_cls.DISTINGUISHED_FOLDER_ID,
                          mailbox=Mailbox(email_address=self.account.primary_smtp_address),
                      ),
                      root=self,
                  )
                  fld.test_access()
                  return self._folders_map.get(fld.id, fld)  # Use cached instance if available
              except MISSING_FOLDER_ERRORS:
                  # The Exchange server does not return a distinguished folder of this type
                  pass
              raise ErrorFolderNotFound(f"No usable default {folder_cls} folders")

          @classmethod
          def get_distinguished(cls, account):
              """Get the distinguished folder for this folder class.

              :param account:
              :return:
              """
              return cls._get_distinguished(
                  folder=cls(
                      _distinguished_id=DistinguishedFolderId(
                          id=cls.DISTINGUISHED_FOLDER_ID,
                          mailbox=Mailbox(email_address=account.primary_smtp_address),
                      ),
                      account=account,
                  )
              )

          @property
          def _folders_map(self):
              if self._subfolders is not None:
                  return self._subfolders

              with self._subfolders_lock:
                  # Map root, and all sub-folders of root, at arbitrary depth by folder ID. First get distinguished folders,
                  # so we are sure to apply the correct Folder class, then fetch all sub-folders of this root.
                  folders_map = {self.id: self}
                  distinguished_folders = [
                      cls(
                          _distinguished_id=DistinguishedFolderId(
                              id=cls.DISTINGUISHED_FOLDER_ID,
                              mailbox=Mailbox(email_address=self.account.primary_smtp_address),
                          ),
                          root=self,
                      )
                      for cls in self.WELLKNOWN_FOLDERS
                      if cls.get_folder_allowed and cls.supports_version(self.account.version)
                  ]
                  for f in FolderCollection(account=self.account, folders=distinguished_folders).resolve():
                      if isinstance(f, MISSING_FOLDER_ERRORS):
                          # This is just a distinguished folder the server does not have
                          continue
                      if isinstance(f, ErrorInvalidOperation):
                          # This is probably a distinguished folder the server does not have. We previously tested the exact
                          # error message (f.value), but some Exchange servers return localized error messages, so that's not
                          # possible to do reliably.
                          continue
                      if isinstance(f, ErrorAccessDenied):
                          # We may not have GetFolder access, either to this folder or at all
                          continue
                      if isinstance(f, Exception):
                          raise f
                      folders_map[f.id] = f
                  for f in (
                      SingleFolderQuerySet(account=self.account, folder=self).depth(self.DEFAULT_FOLDER_TRAVERSAL_DEPTH).all()
                  ):
                      if isinstance(f, ErrorAccessDenied):
                          # We may not have FindFolder access, or GetFolder access, either to this folder or at all
                          continue
                      if isinstance(f, MISSING_FOLDER_ERRORS):
                          # We were unlucky. The folder disappeared between the FindFolder and the GetFolder calls
                          continue
                      if isinstance(f, Exception):
                          raise f
                      if f.id in folders_map:
                          # Already exists. Probably a distinguished folder
                          continue
                      folders_map[f.id] = f
                  self._subfolders = folders_map
                  return folders_map

          @classmethod
          def from_xml(cls, elem, account):
              kwargs = cls._kwargs_from_elem(elem=elem, account=account)
              cls._clear(elem)
              return cls(account=account, **kwargs)

          @classmethod
          def folder_cls_from_folder_name(cls, folder_name, folder_class, locale):
              """Return the folder class that matches a localized folder name. Take into account the 'folder_class' of the
              folder, to not identify an 'IPF.Note' folder as a 'Calendar' class just because it's called e.g. 'Kalender' and
              the locale is 'da_DK'.

              Some folders, e.g. `System`, don't define a `folder_class`. For these folders, we match on localized folder name
              if the folder class does not have its 'CONTAINER_CLASS' set.

              :param folder_name:
              :param folder_class:
              :param locale: a string, e.g. 'da_DK'
              """
              for folder_cls in cls.WELLKNOWN_FOLDERS + NON_DELETABLE_FOLDERS + MISC_FOLDERS:
                  if folder_cls.CONTAINER_CLASS != folder_class:
                      continue
                  if folder_name.lower() not in folder_cls.localized_names(locale):
                      continue
                  return folder_cls
              raise KeyError()

          def __getstate__(self):
              # The lock cannot be pickled
              state = {k: getattr(self, k) for k in self._slots_keys}
              del state["_subfolders_lock"]
              return state

          def __setstate__(self, state):
              # Restore the lock
              for k in self._slots_keys:
                  setattr(self, k, state.get(k))
              self._subfolders_lock = Lock()

          def __repr__(self):
              # Let's not create an infinite loop when printing self.root
              return self.__class__.__name__ + repr(
                  (
                      self.account,
                      "[self]",
                      self.name,
                      self.total_count,
                      self.unread_count,
                      self.child_folder_count,
                      self.folder_class,
                      self.id,
                      self.changekey,
                  )
              )

  Base class for folders that implement the root of a folder hierarchy.

  ### Ancestors

  - BaseFolder
  - RegisterMixIn
  - IdChangeKeyMixIn
  - EWSElement
  - SearchableMixIn
  - SupportedVersionClassMixIn

  ### Subclasses

  - ArchiveRoot
  - PublicFoldersRoot
  - Root

  ### Class variables

  `var FIELDS`
  `var WELLKNOWN_FOLDERS`

  ### Static methods

  `def folder_cls_from_folder_name(folder_name, folder_class, locale)`

    Return the folder class that matches a localized folder name. Take into account the 'folder_class' of the folder, to not identify an 'IPF.Note' folder as a 'Calendar' class just because it's called e.g. 'Kalender' and the locale is 'da_DK'.

    Some folders, e.g. `System`, don't define a `folder_class`. For these folders, we match on localized folder name if the folder class does not have its 'CONTAINER_CLASS' set.

    :param folder_name: :param folder_class: :param locale: a string, e.g. 'da_DK'

  `def from_xml(elem, account)`
  `def get_distinguished(account)`

    Get the distinguished folder for this folder class.

    :param account: :return:

  ### Instance variables

  `var effective_rights`

  ### Methods

  `def add_folder(self, folder)`
    Expand source code

        def add_folder(self, folder):
            if not folder.id:
                raise ValueError("'folder' must have an ID")
            self._folders_map[folder.id] = folder

  `def clear_cache(self)`
    Expand source code

        def clear_cache(self):
            with self._subfolders_lock:
                self._subfolders = None

  `def get_children(self, folder)`
    Expand source code

        def get_children(self, folder):
            for f in self._folders_map.values():
                if not f.parent:
                    continue
                if f.parent.id == folder.id:
                    yield f

  `def get_default_folder(self, folder_cls)`
    Expand source code

        def get_default_folder(self, folder_cls):
            """Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished
            folder was found, try as best we can to return the default folder of type 'folder_cls'
            """
            if not folder_cls.DISTINGUISHED_FOLDER_ID:
                raise ValueError(f"'folder_cls' {folder_cls} must have a DISTINGUISHED_FOLDER_ID value")
            # Use cached distinguished folder instance, but only if cache has already been prepped. This is an optimization
            # for accessing e.g. 'account.contacts' without fetching all folders of the account.
            if self._subfolders is not None:
                for f in self._folders_map.values():
                    # Require exact class, to not match subclasses, e.g. RecipientCache instead of Contacts
                    if f.__class__ == folder_cls and f.is_distinguished:
                        log.debug("Found cached distinguished %s folder", folder_cls)
                        return f
            try:
                log.debug("Requesting distinguished %s folder explicitly", folder_cls)
                return folder_cls.get_distinguished(root=self)
            except ErrorAccessDenied:
                # Maybe we just don't have GetFolder access? Try FindItem instead
                log.debug("Testing default %s folder with FindItem", folder_cls)
                fld = folder_cls(
                    _distinguished_id=DistinguishedFolderId(
                        id=folder_cls.DISTINGUISHED_FOLDER_ID,
                        mailbox=Mailbox(email_address=self.account.primary_smtp_address),
                    ),
                    root=self,
                )
                fld.test_access()
                return self._folders_map.get(fld.id, fld)  # Use cached instance if available
            except MISSING_FOLDER_ERRORS:
                # The Exchange server does not return a distinguished folder of this type
                pass
            raise ErrorFolderNotFound(f"No usable default {folder_cls} folders")

    Return the distinguished folder instance of type folder_cls belonging to this account. If no distinguished folder was found, try as best we can to return the default folder of type 'folder_cls'

  `def get_folder(self, folder)`
    Expand source code

        def get_folder(self, folder):
            if not folder.id:
                raise ValueError("'folder' must have an ID")
            return self._folders_map.get(folder.id)

  `def remove_folder(self, folder)`
    Expand source code

        def remove_folder(self, folder):
            if not folder.id:
                raise ValueError("'folder' must have an ID")
            with suppress(KeyError):
                del self._folders_map[folder.id]

  `def update_folder(self, folder)`
    Expand source code

        def update_folder(self, folder):
            if not folder.id:
                raise ValueError("'folder' must have an ID")
            self._folders_map[folder.id] = folder

  ### Inherited members

  - `BaseFolder`:
    - `ID_ELEMENT_CLS`
    - `account`
    - `add_field`
    - `all`
    - `deregister`
    - `exclude`
    - `filter`
    - `folder_cls_from_container_class`
    - `get`
    - `get_events`
    - `get_streaming_events`
    - `none`
    - `parent`
    - `people`
    - `register`
    - `remove_field`
    - `root`
    - `subscribe_to_pull`
    - `subscribe_to_push`
    - `subscribe_to_streaming`
    - `supported_fields`
    - `sync_hierarchy`
    - `sync_items`
    - `test_access`
    - `tree`
    - `unsubscribe`
    - `validate_field`