exchangelib.folders.base API documentation

# Module `exchangelib.folders.base`

Expand source code

    import abc
    import logging
    from fnmatch import fnmatch
    from operator import attrgetter

    from .collections import FolderCollection, SyncCompleted
    from .queryset import SingleFolderQuerySet, SHALLOW as SHALLOW_FOLDERS, DEEP as DEEP_FOLDERS
    from ..errors import ErrorAccessDenied, ErrorFolderNotFound, ErrorCannotEmptyFolder, ErrorCannotDeleteObject, \
        ErrorDeleteDistinguishedFolder, ErrorInvalidSubscription, ErrorNoPublicFolderReplicaAvailable, ErrorItemNotFound
    from ..fields import IntegerField, CharField, FieldPath, EffectiveRightsField, PermissionSetField, EWSElementField, \
        Field, IdElementField, InvalidField
    from ..items import CalendarItem, RegisterMixIn, ITEM_CLASSES, DELETE_TYPE_CHOICES, HARD_DELETE, \
        SHALLOW as SHALLOW_ITEMS
    from ..properties import Mailbox, FolderId, ParentFolderId, DistinguishedFolderId, UserConfiguration, \
        UserConfigurationName, UserConfigurationNameMNS, EWSMeta
    from ..queryset import SearchableMixIn, DoesNotExist
    from ..services import CreateFolder, UpdateFolder, DeleteFolder, EmptyFolder, GetUserConfiguration, \
        CreateUserConfiguration, UpdateUserConfiguration, DeleteUserConfiguration, SubscribeToPush, SubscribeToPull, \
        Unsubscribe, GetEvents, GetStreamingEvents, MoveFolder
    from ..services.get_user_configuration import ALL
    from ..util import TNS, require_id
    from ..version import Version, EXCHANGE_2007_SP1, EXCHANGE_2010

    log = logging.getLogger(__name__)

    MISSING_FOLDER_ERRORS = (ErrorFolderNotFound, ErrorItemNotFound, ErrorNoPublicFolderReplicaAvailable)

    class BaseFolder(RegisterMixIn, SearchableMixIn, metaclass=EWSMeta):
        """Base class for all classes that implement a folder."""

        ELEMENT_NAME = 'Folder'
        NAMESPACE = TNS
        # See https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
        DISTINGUISHED_FOLDER_ID = None
        # Default item type for this folder. See
        # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxosfld/68a85898-84fe-43c4-b166-4711c13cdd61
        CONTAINER_CLASS = None
        supported_item_models = ITEM_CLASSES  # The Item types that this folder can contain. Default is all
        # Marks the version from which a distinguished folder was introduced. A possibly authoritative source is:
        # https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs
        supported_from = None
        # Whether this folder type is allowed with the GetFolder service
        get_folder_allowed = True
        DEFAULT_FOLDER_TRAVERSAL_DEPTH = DEEP_FOLDERS
        DEFAULT_ITEM_TRAVERSAL_DEPTH = SHALLOW_ITEMS
        LOCALIZED_NAMES = {}  # A map of (str)locale: (tuple)localized_folder_names
        ITEM_MODEL_MAP = {cls.response_tag(): cls for cls in ITEM_CLASSES}
        ID_ELEMENT_CLS = FolderId

        _id = IdElementField(field_uri='folder:FolderId', value_cls=ID_ELEMENT_CLS)
        parent_folder_id = EWSElementField(field_uri='folder:ParentFolderId', value_cls=ParentFolderId,
                                           is_read_only=True)
        folder_class = CharField(field_uri='folder:FolderClass', is_required_after_save=True)
        name = CharField(field_uri='folder:DisplayName')
        total_count = IntegerField(field_uri='folder:TotalCount', is_read_only=True)
        child_folder_count = IntegerField(field_uri='folder:ChildFolderCount', is_read_only=True)
        unread_count = IntegerField(field_uri='folder:UnreadCount', is_read_only=True)

        __slots__ = 'is_distinguished', 'item_sync_state', 'folder_sync_state'

        # Used to register extended properties
        INSERT_AFTER_FIELD = 'child_folder_count'

        def __init__(self, **kwargs):
            self.is_distinguished = kwargs.pop('is_distinguished', False)
            self.item_sync_state = kwargs.pop('item_sync_state', None)
            self.folder_sync_state = kwargs.pop('folder_sync_state', None)
            super().__init__(**kwargs)

        @property
        @abc.abstractmethod
        def account(self):
            pass

        @property
        @abc.abstractmethod
        def root(self):
            pass

        @property
        @abc.abstractmethod
        def parent(self):
            pass

        @property
        def is_deletable(self):
            return not self.is_distinguished

        def clean(self, version=None):
            super().clean(version=version)
            # Set a default folder class for new folders. A folder class cannot be changed after saving.
            if self.id is None and self.folder_class is None:
                self.folder_class = self.CONTAINER_CLASS

        @property
        def children(self):
            # It's dangerous to return a generator here because we may then call methods on a child that result in the
            # cache being updated while it's iterated.
            return FolderCollection(account=self.account, folders=self.root.get_children(self))

        @property
        def parts(self):
            parts = [self]
            f = self.parent
            while f:
                parts.insert(0, f)
                f = f.parent
            return parts

        @property
        def absolute(self):
            return ''.join('/%s' % p.name for p in self.parts)

        def _walk(self):
            for c in self.children:
                yield c
                yield from c.walk()

        def walk(self):
            return FolderCollection(account=self.account, folders=self._walk())

        def _glob(self, pattern):
            split_pattern = pattern.rsplit('/', 1)
            head, tail = (split_pattern[0], None) if len(split_pattern) == 1 else split_pattern
            if head == '':
                # We got an absolute path. Restart globbing at root
                yield from self.root.glob(tail or '*')
            elif head == '..':
                # Relative path with reference to parent. Restart globbing at parent
                if not self.parent:
                    raise ValueError('Already at top')
                yield from self.parent.glob(tail or '*')
            elif head == '**':
                # Match anything here or in any subfolder at arbitrary depth
                for c in self.walk():
                    # fnmatch() may be case-sensitive depending on operating system:
                    # force a case-insensitive match since case appears not to
                    # matter for folders in Exchange
                    if fnmatch(c.name.lower(), (tail or '*').lower()):
                        yield c
            else:
                # Regular pattern
                for c in self.children:
                    # See note above on fnmatch() case-sensitivity
                    if not fnmatch(c.name.lower(), head.lower()):
                        continue
                    if tail is None:
                        yield c
                        continue
                    yield from c.glob(tail)

        def glob(self, pattern):
            return FolderCollection(account=self.account, folders=self._glob(pattern))

        def tree(self):
            """Return a string representation of the folder structure of this folder. Example:

            root
            ├── inbox
            │   └── todos
            └── archive
                ├── Last Job
                ├── exchangelib issues
                └── Mom
            """
            tree = '%s\n' % self.name
            children = list(self.children)
            for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
                nodes = c.tree().split('\n')
                for j, node in enumerate(nodes, start=1):
                    if i != len(children) and j == 1:
                        # Not the last child, but the first node, which is the name of the child
                        tree += '├── %s\n' % node
                    elif i != len(children) and j > 1:
                        # Not the last child, and not name of child
                        tree += '│   %s\n' % node
                    elif i == len(children) and j == 1:
                        # Not the last child, but the first node, which is the name of the child
                        tree += '└── %s\n' % node
                    else:  # Last child, and not name of child
                        tree += '    %s\n' % node
            return tree.strip()

        @classmethod
        def supports_version(cls, version):
            # 'version' is a Version instance, for convenience by callers
            if not isinstance(version, Version):
                raise ValueError("'version' %r must be a Version instance" % version)
            if not cls.supported_from:
                return True
            return version.build >= cls.supported_from

        @property
        def has_distinguished_name(self):
            return self.name and self.DISTINGUISHED_FOLDER_ID and self.name.lower() == self.DISTINGUISHED_FOLDER_ID.lower()

        @classmethod
        def localized_names(cls, locale):
            # Return localized names for a specific locale. If no locale-specific names exist, return the default names,
            # if any.
            return tuple(s.lower() for s in cls.LOCALIZED_NAMES.get(locale, cls.LOCALIZED_NAMES.get(None, [])))

        @staticmethod
        def folder_cls_from_container_class(container_class):
            """Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS
            because many folder classes have the same CONTAINER_CLASS.

            :param container_class:
            :return:
            """
            from .known_folders import Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, \
                RecipientCache, RSSFeeds
            for folder_cls in (
                    Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, RecipientCache,
                    RSSFeeds):
                if folder_cls.CONTAINER_CLASS == container_class:
                    return folder_cls
            raise KeyError()

        @classmethod
        def item_model_from_tag(cls, tag):
            try:
                return cls.ITEM_MODEL_MAP[tag]
            except KeyError:
                raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))

        @classmethod
        def allowed_item_fields(cls, version):
            # Return non-ID fields of all item classes allowed in this folder type
            fields = set()
            for item_model in cls.supported_item_models:
                fields.update(
                    set(item_model.supported_fields(version=version))
                )
            return fields

        def validate_item_field(self, field, version):
            # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
            # for the item types supported by this folder.

            # For each field, check if the field is valid for any of the item models supported by this folder
            for item_model in self.supported_item_models:
                try:
                    item_model.validate_field(field=field, version=version)
                    break
                except InvalidField:
                    continue
            else:
                raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

        def normalize_fields(self, fields):
            # Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
            # objects and adds internal timezone fields if necessary. Assume fields are already validated.
            fields = list(fields)
            has_start, has_end = False, False
            for i, field_path in enumerate(fields):
                # Allow both Field and FieldPath instances and string field paths as input
                if isinstance(field_path, str):
                    field_path = FieldPath.from_string(field_path=field_path, folder=self)
                    fields[i] = field_path
                elif isinstance(field_path, Field):
                    field_path = FieldPath(field=field_path)
                    fields[i] = field_path
                if not isinstance(field_path, FieldPath):
                    raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
                if field_path.field.name == 'start':
                    has_start = True
                elif field_path.field.name == 'end':
                    has_end = True

            # For CalendarItem items, we want to inject internal timezone fields. See also CalendarItem.clean()
            if CalendarItem in self.supported_item_models:
                meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
                if self.account.version.build < EXCHANGE_2010:
                    if has_start or has_end:
                        fields.append(FieldPath(field=meeting_tz_field))
                else:
                    if has_start:
                        fields.append(FieldPath(field=start_tz_field))
                    if has_end:
                        fields.append(FieldPath(field=end_tz_field))
            return fields

        @classmethod
        def get_item_field_by_fieldname(cls, fieldname):
            for item_model in cls.supported_item_models:
                try:
                    return item_model.get_field_by_fieldname(fieldname)
                except InvalidField:
                    pass
            raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))

        def get(self, *args, **kwargs):
            return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)

        def all(self):
            return FolderCollection(account=self.account, folders=[self]).all()

        def none(self):
            return FolderCollection(account=self.account, folders=[self]).none()

        def filter(self, *args, **kwargs):
            return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)

        def exclude(self, *args, **kwargs):
            return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)

        def people(self):
            # No point in using a FolderCollection because FindPeople only supports one folder
            return FolderCollection(account=self.account, folders=[self]).people()

        def bulk_create(self, items, *args, **kwargs):
            return self.account.bulk_create(folder=self, items=items, *args, **kwargs)

        def save(self, update_fields=None):
            if self.id is None:
                # New folder
                if update_fields:
                    raise ValueError("'update_fields' is only valid for updates")
                res = CreateFolder(account=self.account).get(parent_folder=self.parent, folders=[self])
                self._id = self.ID_ELEMENT_CLS(res.id, res.changekey)
                self.root.add_folder(self)  # Add this folder to the cache
                return self

            # Update folder
            if not update_fields:
                # The fields to update was not specified explicitly. Update all fields where update is possible
                update_fields = []
                for f in self.supported_fields(version=self.account.version):
                    if f.is_read_only:
                        # These cannot be changed
                        continue
                    if (f.is_required or f.is_required_after_save) and (
                            getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
                    ):
                        # These are required and cannot be deleted
                        continue
                    update_fields.append(f.name)
            res = UpdateFolder(account=self.account).get(folders=[(self, update_fields)])
            folder_id, changekey = res.id, res.changekey
            if self.id != folder_id:
                raise ValueError('ID mismatch')
            # Don't check changekey value. It may not change on no-op updates
            self.changekey = changekey
            self.root.update_folder(self)  # Update the folder in the cache
            return self

        def move(self, to_folder):
            res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
            folder_id, changekey = res.id, res.changekey
            if self.id != folder_id:
                raise ValueError('ID mismatch')
            # Don't check changekey value. It may not change on no-op moves
            self.changekey = changekey
            self.parent_folder_id = ParentFolderId(id=to_folder.id, changekey=to_folder.changekey)
            self.root.update_folder(self)  # Update the folder in the cache

        def delete(self, delete_type=HARD_DELETE):
            if delete_type not in DELETE_TYPE_CHOICES:
                raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
            DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
            self.root.remove_folder(self)  # Remove the updated folder from the cache
            self._id = None

        def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
            if delete_type not in DELETE_TYPE_CHOICES:
                raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
            EmptyFolder(account=self.account).get(
                folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
            )
            if delete_sub_folders:
                # We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
                self.root.clear_cache()

        def wipe(self, page_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!
            _seen = _seen or set()
            if self.id in _seen:
                raise RecursionError('We already tried to wipe %s' % self)
            if _level > 16:
                raise RecursionError('Max recursion level reached: %s' % _level)
            _seen.add(self.id)
            log.warning('Wiping %s', self)
            has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
            try:
                if has_distinguished_subfolders:
                    self.empty(delete_sub_folders=False)
                else:
                    self.empty(delete_sub_folders=True)
            except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
                try:
                    if has_distinguished_subfolders:
                        raise  # We already tried this
                    self.empty(delete_sub_folders=False)
                except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
                    log.warning('Not allowed to empty %s. Trying to delete items instead', self)
                    try:
                        self.all().delete(**dict(page_size=page_size) if page_size else {})
                    except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
                        log.warning('Not allowed to delete items in %s', self)
            _level += 1
            for f in self.children:
                f.wipe(page_size=page_size, _seen=_seen, _level=_level)
                # Remove non-distinguished children that are empty and have no subfolders
                if f.is_deletable and not f.children:
                    log.warning('Deleting folder %s', f)
                    try:
                        f.delete()
                    except ErrorDeleteDistinguishedFolder:
                        log.warning('Tried to delete a distinguished folder (%s)', f)

        def test_access(self):
            """Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the
            service user doesn't have access to the calendar. This will throw the most common errors.
            """
            self.all().exists()
            return True

        @classmethod
        def _kwargs_from_elem(cls, elem, account):
            # Check for 'DisplayName' element before collecting kwargs because because that clears the elements
            has_name_elem = elem.find(cls.get_field_by_fieldname('name').response_tag()) is not None
            kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
            if has_name_elem and not kwargs['name']:
                # When we request the 'DisplayName' property, some folders may still be returned with an empty value.
                # Assign a default name to these folders.
                kwargs['name'] = cls.DISTINGUISHED_FOLDER_ID
            return kwargs

        def to_folder_id(self):
            if self.is_distinguished:
                # Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
                # the folder content since we fetched the changekey.
                if self.account:
                    return DistinguishedFolderId(
                        id=self.DISTINGUISHED_FOLDER_ID,
                        mailbox=Mailbox(email_address=self.account.primary_smtp_address)
                    )
                return DistinguishedFolderId(id=self.DISTINGUISHED_FOLDER_ID)
            if self.id:
                return FolderId(id=self.id, changekey=self.changekey)
            raise ValueError('Must be a distinguished folder or have an ID')

        def to_xml(self, version):
            try:
                return self.to_folder_id().to_xml(version=version)
            except ValueError:
                return super().to_xml(version=version)

        def to_id_xml(self, version):
            # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
            return self.to_xml(version=version)

        @classmethod
        def resolve(cls, account, folder):
            # Resolve a single folder
            folders = list(FolderCollection(account=account, folders=[folder]).resolve())
            if not folders:
                raise ErrorFolderNotFound('Could not find folder %r' % folder)
            if len(folders) != 1:
                raise ValueError('Expected result length 1, but got %s' % folders)
            f = folders[0]
            if isinstance(f, Exception):
                raise f
            if f.__class__ != cls:
                raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
            return f

        @require_id
        def refresh(self):
            fresh_folder = self.resolve(account=self.account, folder=self)
            if self.id != fresh_folder.id:
                raise ValueError('ID mismatch')
            # Apparently, the changekey may get updated
            for f in self.FIELDS:
                setattr(self, f.name, getattr(fresh_folder, f.name))
            return self

        @require_id
        def get_user_configuration(self, name, properties=ALL):
            return GetUserConfiguration(account=self.account).get(
                user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
                properties=properties,
            )

        @require_id
        def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
            user_configuration = UserConfiguration(
                user_configuration_name=UserConfigurationName(name=name, folder=self),
                dictionary=dictionary,
                xml_data=xml_data,
                binary_data=binary_data,
            )
            return CreateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

        @require_id
        def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
            user_configuration = UserConfiguration(
                user_configuration_name=UserConfigurationName(name=name, folder=self),
                dictionary=dictionary,
                xml_data=xml_data,
                binary_data=binary_data,
            )
            return UpdateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

        @require_id
        def delete_user_configuration(self, name):
            return DeleteUserConfiguration(account=self.account).get(
                user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
            )

        @require_id
        def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
            """Create a pull subscription.

            :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES
            :param watermark: An event bookmark as returned by some sync services
            :param timeout: Timeout of the subscription, in minutes. Timeout is reset when the server receives a
            GetEvents request for this subscription.
            :return: The subscription ID and a watermark
            """
            s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
                event_types=event_types, watermark=watermark, timeout=timeout,
            ))
            if len(s_ids) != 1:
                raise ValueError('Expected result length 1, but got %s' % s_ids)
            s_id = s_ids[0]
            if isinstance(s_id, Exception):
                raise s_id
            return s_id

        @require_id
        def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                              status_frequency=1):
            """Create a push subscription.

            :param callback_url: A client-defined URL that the server will call
            :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
            :param watermark: An event bookmark as returned by some sync services
            :param status_frequency: The frequency, in minutes, that the callback URL will be called with.
            :return: The subscription ID and a watermark
            """
            s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
                event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
            ))
            if len(s_ids) != 1:
                raise ValueError('Expected result length 1, but got %s' % s_ids)
            s_id = s_ids[0]
            if isinstance(s_id, Exception):
                raise s_id
            return s_id

        @require_id
        def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
            """Create a streaming subscription.

            :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
            :return: The subscription ID
            """
            s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
                event_types=event_types,
            ))
            if len(s_ids) != 1:
                raise ValueError('Expected result length 1, but got %s' % s_ids)
            s_id = s_ids[0]
            if isinstance(s_id, Exception):
                raise s_id
            return s_id

        @require_id
        def pull_subscription(self, **kwargs):
            return PullSubscription(folder=self, **kwargs)

        @require_id
        def push_subscription(self, **kwargs):
            return PushSubscription(folder=self, **kwargs)

        @require_id
        def streaming_subscription(self, **kwargs):
            return StreamingSubscription(folder=self, **kwargs)

        def unsubscribe(self, subscription_id):
            """Unsubscribe. Only applies to pull and streaming notifications.

            :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]()
            :return: True

            This method doesn't need the current folder instance, but it makes sense to keep the method along the other
            sync methods.
            """
            return Unsubscribe(account=self.account).get(subscription_id=subscription_id)

        def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
            """Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
            this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

            :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
            :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
            :param ignore: A list of Item IDs to ignore in the sync
            :param max_changes_returned: The max number of change
            :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
               values are specified in SyncFolderitems.SYNC_SCOPES
            :return: A generator of (change_type, item) tuples
            """
            if not sync_state:
                sync_state = self.item_sync_state
            try:
                yield from FolderCollection(account=self.account, folders=[self]).sync_items(
                    sync_state=sync_state,
                    only_fields=only_fields,
                    ignore=ignore,
                    max_changes_returned=max_changes_returned,
                    sync_scope=sync_scope,
                )
            except SyncCompleted as e:
                # Set the new sync state on the folder instance
                self.item_sync_state = e.sync_state

        def sync_hierarchy(self, sync_state=None, only_fields=None):
            """Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
            changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
            sync state.

            :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
            :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
            :return:
            """
            if not sync_state:
                sync_state = self.folder_sync_state
            try:
                yield from FolderCollection(account=self.account, folders=[self]).sync_hierarchy(
                    sync_state=sync_state,
                    only_fields=only_fields,
                )
            except SyncCompleted as e:
                # Set the new sync state on the folder instance
                self.folder_sync_state = e.sync_state

        def get_events(self, subscription_id, watermark):
            """Get events since the given watermark. Non-blocking.

            :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|push]()
            :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call.
            :return: A Notification object containing a list of events

            This method doesn't need the current folder instance, but it makes sense to keep the method along the other
            sync methods.
            """
            svc = GetEvents(account=self.account)
            while True:
                notification = svc.get(subscription_id=subscription_id, watermark=watermark)
                yield notification
                if not notification.more_events:
                    break

        def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
            """Get events since the subscription was created, in streaming mode. This method will block as many minutes
            as specified by 'connection_timeout'.

            :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
            :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
            is reached.
            :param max_notifications_returned: If specified, will exit after receiving this number of notifications
            :return: A generator of Notification objects, each containing a list of events

            This method doesn't need the current folder instance, but it makes sense to keep the method along the other
            sync methods.
            """
            # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
            request_timeout = connection_timeout*60 + 60
            svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
            for i, notification in enumerate(
                    svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
                    start=1
            ):
                yield notification
                if max_notifications_returned and i >= max_notifications_returned:
                    svc.stop_streaming()
                    break
            if svc.error_subscription_ids:
                raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)

        def __floordiv__(self, other):
            """Support the some_folder // 'child_folder' // 'child_of_child_folder' navigation syntax.

            Works like as __truediv__ but does not touch the folder cache.

            This is useful if the folder hierarchy contains a huge number of folders and you don't want to fetch them all

            :param other:
            :return:
            """
            if other == '..':
                raise ValueError('Cannot get parent without a folder cache')

            if other == '.':
                return self

            # Assume an exact match on the folder name in a shallow search will only return at most one folder
            try:
                return SingleFolderQuerySet(account=self.account, folder=self).depth(SHALLOW_FOLDERS).get(name=other)
            except DoesNotExist:
                raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

        def __truediv__(self, other):
            """Support the some_folder / 'child_folder' / 'child_of_child_folder' navigation syntax."""
            if other == '..':
                if not self.parent:
                    raise ValueError('Already at top')
                return self.parent
            if other == '.':
                return self
            for c in self.children:
                if c.name == other:
                    return c
            raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

        def __repr__(self):
            return self.__class__.__name__ + \
                   repr((self.root, self.name, self.total_count, self.unread_count, self.child_folder_count,
                         self.folder_class, self.id, self.changekey))

        def __str__(self):
            return '%s (%s)' % (self.__class__.__name__, self.name)

    class Folder(BaseFolder):
        """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folder"""

        permission_set = PermissionSetField(field_uri='folder:PermissionSet', supported_from=EXCHANGE_2007_SP1)
        effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                                supported_from=EXCHANGE_2007_SP1)

        __slots__ = '_root',

        def __init__(self, **kwargs):
            self._root = kwargs.pop('root', None)  # This is a pointer to the root of the folder hierarchy
            parent = kwargs.pop('parent', None)
            if parent:
                if self.root:
                    if parent.root != self.root:
                        raise ValueError("'parent.root' must match 'root'")
                else:
                    self.root = parent.root
                if 'parent_folder_id' in kwargs and parent.id != kwargs['parent_folder_id']:
                    raise ValueError("'parent_folder_id' must match 'parent' ID")
                kwargs['parent_folder_id'] = ParentFolderId(id=parent.id, changekey=parent.changekey)
            super().__init__(**kwargs)

        @property
        def account(self):
            if self.root is None:
                return None
            return self.root.account

        @property
        def root(self):
            return self._root

        @root.setter
        def root(self, value):
            self._root = value

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

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

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

            :param root:
            :return:
            """
            try:
                return cls.resolve(
                    account=root.account,
                    folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
                )
            except MISSING_FOLDER_ERRORS:
                raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

        @property
        def parent(self):
            if not self.parent_folder_id:
                return None
            if self.parent_folder_id.id == self.id:
                # Some folders have a parent that references itself. Avoid circular references here
                return None
            return self.root.get_folder(self.parent_folder_id)

        @parent.setter
        def parent(self, value):
            if value is None:
                self.parent_folder_id = None
            else:
                if not isinstance(value, BaseFolder):
                    raise ValueError("'value' %r must be a Folder instance" % value)
                self.root = value.root
                self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey)

        def clean(self, version=None):
            from .roots import RootOfHierarchy
            super().clean(version=version)
            if self.root and not isinstance(self.root, RootOfHierarchy):
                raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

        @classmethod
        def from_xml_with_root(cls, elem, root):
            folder = cls.from_xml(elem=elem, account=root.account)
            folder_cls = cls
            if cls == Folder:
                # We were called on the generic Folder class. Try to find a more specific class to return objects as.
                #
                # The "FolderClass" element value is the only indication we have in the FindFolder response of which
                # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
                # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
                # folders types for e.g. Account.inbox and Account.trash.
                #
                # We should be able to just use the name, but apparently default folder names can be renamed to a set of
                # localized names using a PowerShell command:
                # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
                #
                # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
                # folder class by the "FolderClass" value.
                #
                # The returned XML may contain neither folder class nor name. In that case, we default to the generic
                # Folder class.
                if folder.name:
                    try:
                        # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                        folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                                      locale=root.account.locale)
                        log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
                    except KeyError:
                        pass
                if folder.folder_class and folder_cls == Folder:
                    try:
                        folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                        log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                                  folder.name)
                    except KeyError:
                        pass
                if folder_cls == Folder:
                    log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
            return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})

    class BaseSubscription(metaclass=abc.ABCMeta):
        def __init__(self, folder, **subscription_kwargs):
            self.folder = folder
            self.subscription_kwargs = subscription_kwargs
            self.subscription_id = None

        def __enter__(self):
            pass

        def __exit__(self, *args, **kwargs):
            self.folder.unsubscribe(subscription_id=self.subscription_id)
            self.subscription_id = None

    class PullSubscription(BaseSubscription):
        def __enter__(self):
            self.subscription_id, watermark = self.folder.subscribe_to_pull(**self.subscription_kwargs)
            return self.subscription_id, watermark

    class PushSubscription(BaseSubscription):
        def __enter__(self):
            self.subscription_id, watermark = self.folder.subscribe_to_push(**self.subscription_kwargs)
            return self.subscription_id, watermark

        def __exit__(self, *args, **kwargs):
            # Cannot unsubscribe to push subscriptions
            pass

    class StreamingSubscription(BaseSubscription):
        def __enter__(self):
            self.subscription_id = self.folder.subscribe_to_streaming(**self.subscription_kwargs)
            return self.subscription_id

## Classes

`class BaseFolder (**kwargs)`

  Base class for all classes that implement a folder.

  Expand source code

      class BaseFolder(RegisterMixIn, SearchableMixIn, metaclass=EWSMeta):
          """Base class for all classes that implement a folder."""

          ELEMENT_NAME = 'Folder'
          NAMESPACE = TNS
          # See https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid
          DISTINGUISHED_FOLDER_ID = None
          # Default item type for this folder. See
          # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxosfld/68a85898-84fe-43c4-b166-4711c13cdd61
          CONTAINER_CLASS = None
          supported_item_models = ITEM_CLASSES  # The Item types that this folder can contain. Default is all
          # Marks the version from which a distinguished folder was introduced. A possibly authoritative source is:
          # https://github.com/OfficeDev/ews-managed-api/blob/master/Enumerations/WellKnownFolderName.cs
          supported_from = None
          # Whether this folder type is allowed with the GetFolder service
          get_folder_allowed = True
          DEFAULT_FOLDER_TRAVERSAL_DEPTH = DEEP_FOLDERS
          DEFAULT_ITEM_TRAVERSAL_DEPTH = SHALLOW_ITEMS
          LOCALIZED_NAMES = {}  # A map of (str)locale: (tuple)localized_folder_names
          ITEM_MODEL_MAP = {cls.response_tag(): cls for cls in ITEM_CLASSES}
          ID_ELEMENT_CLS = FolderId

          _id = IdElementField(field_uri='folder:FolderId', value_cls=ID_ELEMENT_CLS)
          parent_folder_id = EWSElementField(field_uri='folder:ParentFolderId', value_cls=ParentFolderId,
                                             is_read_only=True)
          folder_class = CharField(field_uri='folder:FolderClass', is_required_after_save=True)
          name = CharField(field_uri='folder:DisplayName')
          total_count = IntegerField(field_uri='folder:TotalCount', is_read_only=True)
          child_folder_count = IntegerField(field_uri='folder:ChildFolderCount', is_read_only=True)
          unread_count = IntegerField(field_uri='folder:UnreadCount', is_read_only=True)

          __slots__ = 'is_distinguished', 'item_sync_state', 'folder_sync_state'

          # Used to register extended properties
          INSERT_AFTER_FIELD = 'child_folder_count'

          def __init__(self, **kwargs):
              self.is_distinguished = kwargs.pop('is_distinguished', False)
              self.item_sync_state = kwargs.pop('item_sync_state', None)
              self.folder_sync_state = kwargs.pop('folder_sync_state', None)
              super().__init__(**kwargs)

          @property
          @abc.abstractmethod
          def account(self):
              pass

          @property
          @abc.abstractmethod
          def root(self):
              pass

          @property
          @abc.abstractmethod
          def parent(self):
              pass

          @property
          def is_deletable(self):
              return not self.is_distinguished

          def clean(self, version=None):
              super().clean(version=version)
              # Set a default folder class for new folders. A folder class cannot be changed after saving.
              if self.id is None and self.folder_class is None:
                  self.folder_class = self.CONTAINER_CLASS

          @property
          def children(self):
              # It's dangerous to return a generator here because we may then call methods on a child that result in the
              # cache being updated while it's iterated.
              return FolderCollection(account=self.account, folders=self.root.get_children(self))

          @property
          def parts(self):
              parts = [self]
              f = self.parent
              while f:
                  parts.insert(0, f)
                  f = f.parent
              return parts

          @property
          def absolute(self):
              return ''.join('/%s' % p.name for p in self.parts)

          def _walk(self):
              for c in self.children:
                  yield c
                  yield from c.walk()

          def walk(self):
              return FolderCollection(account=self.account, folders=self._walk())

          def _glob(self, pattern):
              split_pattern = pattern.rsplit('/', 1)
              head, tail = (split_pattern[0], None) if len(split_pattern) == 1 else split_pattern
              if head == '':
                  # We got an absolute path. Restart globbing at root
                  yield from self.root.glob(tail or '*')
              elif head == '..':
                  # Relative path with reference to parent. Restart globbing at parent
                  if not self.parent:
                      raise ValueError('Already at top')
                  yield from self.parent.glob(tail or '*')
              elif head == '**':
                  # Match anything here or in any subfolder at arbitrary depth
                  for c in self.walk():
                      # fnmatch() may be case-sensitive depending on operating system:
                      # force a case-insensitive match since case appears not to
                      # matter for folders in Exchange
                      if fnmatch(c.name.lower(), (tail or '*').lower()):
                          yield c
              else:
                  # Regular pattern
                  for c in self.children:
                      # See note above on fnmatch() case-sensitivity
                      if not fnmatch(c.name.lower(), head.lower()):
                          continue
                      if tail is None:
                          yield c
                          continue
                      yield from c.glob(tail)

          def glob(self, pattern):
              return FolderCollection(account=self.account, folders=self._glob(pattern))

          def tree(self):
              """Return a string representation of the folder structure of this folder. Example:

              root
              ├── inbox
              │   └── todos
              └── archive
                  ├── Last Job
                  ├── exchangelib issues
                  └── Mom
              """
              tree = '%s\n' % self.name
              children = list(self.children)
              for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
                  nodes = c.tree().split('\n')
                  for j, node in enumerate(nodes, start=1):
                      if i != len(children) and j == 1:
                          # Not the last child, but the first node, which is the name of the child
                          tree += '├── %s\n' % node
                      elif i != len(children) and j > 1:
                          # Not the last child, and not name of child
                          tree += '│   %s\n' % node
                      elif i == len(children) and j == 1:
                          # Not the last child, but the first node, which is the name of the child
                          tree += '└── %s\n' % node
                      else:  # Last child, and not name of child
                          tree += '    %s\n' % node
              return tree.strip()

          @classmethod
          def supports_version(cls, version):
              # 'version' is a Version instance, for convenience by callers
              if not isinstance(version, Version):
                  raise ValueError("'version' %r must be a Version instance" % version)
              if not cls.supported_from:
                  return True
              return version.build >= cls.supported_from

          @property
          def has_distinguished_name(self):
              return self.name and self.DISTINGUISHED_FOLDER_ID and self.name.lower() == self.DISTINGUISHED_FOLDER_ID.lower()

          @classmethod
          def localized_names(cls, locale):
              # Return localized names for a specific locale. If no locale-specific names exist, return the default names,
              # if any.
              return tuple(s.lower() for s in cls.LOCALIZED_NAMES.get(locale, cls.LOCALIZED_NAMES.get(None, [])))

          @staticmethod
          def folder_cls_from_container_class(container_class):
              """Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS
              because many folder classes have the same CONTAINER_CLASS.

              :param container_class:
              :return:
              """
              from .known_folders import Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, \
                  RecipientCache, RSSFeeds
              for folder_cls in (
                      Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, RecipientCache,
                      RSSFeeds):
                  if folder_cls.CONTAINER_CLASS == container_class:
                      return folder_cls
              raise KeyError()

          @classmethod
          def item_model_from_tag(cls, tag):
              try:
                  return cls.ITEM_MODEL_MAP[tag]
              except KeyError:
                  raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))

          @classmethod
          def allowed_item_fields(cls, version):
              # Return non-ID fields of all item classes allowed in this folder type
              fields = set()
              for item_model in cls.supported_item_models:
                  fields.update(
                      set(item_model.supported_fields(version=version))
                  )
              return fields

          def validate_item_field(self, field, version):
              # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
              # for the item types supported by this folder.

              # For each field, check if the field is valid for any of the item models supported by this folder
              for item_model in self.supported_item_models:
                  try:
                      item_model.validate_field(field=field, version=version)
                      break
                  except InvalidField:
                      continue
              else:
                  raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

          def normalize_fields(self, fields):
              # Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
              # objects and adds internal timezone fields if necessary. Assume fields are already validated.
              fields = list(fields)
              has_start, has_end = False, False
              for i, field_path in enumerate(fields):
                  # Allow both Field and FieldPath instances and string field paths as input
                  if isinstance(field_path, str):
                      field_path = FieldPath.from_string(field_path=field_path, folder=self)
                      fields[i] = field_path
                  elif isinstance(field_path, Field):
                      field_path = FieldPath(field=field_path)
                      fields[i] = field_path
                  if not isinstance(field_path, FieldPath):
                      raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
                  if field_path.field.name == 'start':
                      has_start = True
                  elif field_path.field.name == 'end':
                      has_end = True

              # For CalendarItem items, we want to inject internal timezone fields. See also CalendarItem.clean()
              if CalendarItem in self.supported_item_models:
                  meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
                  if self.account.version.build < EXCHANGE_2010:
                      if has_start or has_end:
                          fields.append(FieldPath(field=meeting_tz_field))
                  else:
                      if has_start:
                          fields.append(FieldPath(field=start_tz_field))
                      if has_end:
                          fields.append(FieldPath(field=end_tz_field))
              return fields

          @classmethod
          def get_item_field_by_fieldname(cls, fieldname):
              for item_model in cls.supported_item_models:
                  try:
                      return item_model.get_field_by_fieldname(fieldname)
                  except InvalidField:
                      pass
              raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))

          def get(self, *args, **kwargs):
              return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)

          def all(self):
              return FolderCollection(account=self.account, folders=[self]).all()

          def none(self):
              return FolderCollection(account=self.account, folders=[self]).none()

          def filter(self, *args, **kwargs):
              return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)

          def exclude(self, *args, **kwargs):
              return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)

          def people(self):
              # No point in using a FolderCollection because FindPeople only supports one folder
              return FolderCollection(account=self.account, folders=[self]).people()

          def bulk_create(self, items, *args, **kwargs):
              return self.account.bulk_create(folder=self, items=items, *args, **kwargs)

          def save(self, update_fields=None):
              if self.id is None:
                  # New folder
                  if update_fields:
                      raise ValueError("'update_fields' is only valid for updates")
                  res = CreateFolder(account=self.account).get(parent_folder=self.parent, folders=[self])
                  self._id = self.ID_ELEMENT_CLS(res.id, res.changekey)
                  self.root.add_folder(self)  # Add this folder to the cache
                  return self

              # Update folder
              if not update_fields:
                  # The fields to update was not specified explicitly. Update all fields where update is possible
                  update_fields = []
                  for f in self.supported_fields(version=self.account.version):
                      if f.is_read_only:
                          # These cannot be changed
                          continue
                      if (f.is_required or f.is_required_after_save) and (
                              getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
                      ):
                          # These are required and cannot be deleted
                          continue
                      update_fields.append(f.name)
              res = UpdateFolder(account=self.account).get(folders=[(self, update_fields)])
              folder_id, changekey = res.id, res.changekey
              if self.id != folder_id:
                  raise ValueError('ID mismatch')
              # Don't check changekey value. It may not change on no-op updates
              self.changekey = changekey
              self.root.update_folder(self)  # Update the folder in the cache
              return self

          def move(self, to_folder):
              res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
              folder_id, changekey = res.id, res.changekey
              if self.id != folder_id:
                  raise ValueError('ID mismatch')
              # Don't check changekey value. It may not change on no-op moves
              self.changekey = changekey
              self.parent_folder_id = ParentFolderId(id=to_folder.id, changekey=to_folder.changekey)
              self.root.update_folder(self)  # Update the folder in the cache

          def delete(self, delete_type=HARD_DELETE):
              if delete_type not in DELETE_TYPE_CHOICES:
                  raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
              DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
              self.root.remove_folder(self)  # Remove the updated folder from the cache
              self._id = None

          def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
              if delete_type not in DELETE_TYPE_CHOICES:
                  raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
              EmptyFolder(account=self.account).get(
                  folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
              )
              if delete_sub_folders:
                  # We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
                  self.root.clear_cache()

          def wipe(self, page_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!
              _seen = _seen or set()
              if self.id in _seen:
                  raise RecursionError('We already tried to wipe %s' % self)
              if _level > 16:
                  raise RecursionError('Max recursion level reached: %s' % _level)
              _seen.add(self.id)
              log.warning('Wiping %s', self)
              has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
              try:
                  if has_distinguished_subfolders:
                      self.empty(delete_sub_folders=False)
                  else:
                      self.empty(delete_sub_folders=True)
              except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
                  try:
                      if has_distinguished_subfolders:
                          raise  # We already tried this
                      self.empty(delete_sub_folders=False)
                  except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
                      log.warning('Not allowed to empty %s. Trying to delete items instead', self)
                      try:
                          self.all().delete(**dict(page_size=page_size) if page_size else {})
                      except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
                          log.warning('Not allowed to delete items in %s', self)
              _level += 1
              for f in self.children:
                  f.wipe(page_size=page_size, _seen=_seen, _level=_level)
                  # Remove non-distinguished children that are empty and have no subfolders
                  if f.is_deletable and not f.children:
                      log.warning('Deleting folder %s', f)
                      try:
                          f.delete()
                      except ErrorDeleteDistinguishedFolder:
                          log.warning('Tried to delete a distinguished folder (%s)', f)

          def test_access(self):
              """Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the
              service user doesn't have access to the calendar. This will throw the most common errors.
              """
              self.all().exists()
              return True

          @classmethod
          def _kwargs_from_elem(cls, elem, account):
              # Check for 'DisplayName' element before collecting kwargs because because that clears the elements
              has_name_elem = elem.find(cls.get_field_by_fieldname('name').response_tag()) is not None
              kwargs = {f.name: f.from_xml(elem=elem, account=account) for f in cls.FIELDS}
              if has_name_elem and not kwargs['name']:
                  # When we request the 'DisplayName' property, some folders may still be returned with an empty value.
                  # Assign a default name to these folders.
                  kwargs['name'] = cls.DISTINGUISHED_FOLDER_ID
              return kwargs

          def to_folder_id(self):
              if self.is_distinguished:
                  # Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
                  # the folder content since we fetched the changekey.
                  if self.account:
                      return DistinguishedFolderId(
                          id=self.DISTINGUISHED_FOLDER_ID,
                          mailbox=Mailbox(email_address=self.account.primary_smtp_address)
                      )
                  return DistinguishedFolderId(id=self.DISTINGUISHED_FOLDER_ID)
              if self.id:
                  return FolderId(id=self.id, changekey=self.changekey)
              raise ValueError('Must be a distinguished folder or have an ID')

          def to_xml(self, version):
              try:
                  return self.to_folder_id().to_xml(version=version)
              except ValueError:
                  return super().to_xml(version=version)

          def to_id_xml(self, version):
              # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
              return self.to_xml(version=version)

          @classmethod
          def resolve(cls, account, folder):
              # Resolve a single folder
              folders = list(FolderCollection(account=account, folders=[folder]).resolve())
              if not folders:
                  raise ErrorFolderNotFound('Could not find folder %r' % folder)
              if len(folders) != 1:
                  raise ValueError('Expected result length 1, but got %s' % folders)
              f = folders[0]
              if isinstance(f, Exception):
                  raise f
              if f.__class__ != cls:
                  raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
              return f

          @require_id
          def refresh(self):
              fresh_folder = self.resolve(account=self.account, folder=self)
              if self.id != fresh_folder.id:
                  raise ValueError('ID mismatch')
              # Apparently, the changekey may get updated
              for f in self.FIELDS:
                  setattr(self, f.name, getattr(fresh_folder, f.name))
              return self

          @require_id
          def get_user_configuration(self, name, properties=ALL):
              return GetUserConfiguration(account=self.account).get(
                  user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
                  properties=properties,
              )

          @require_id
          def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
              user_configuration = UserConfiguration(
                  user_configuration_name=UserConfigurationName(name=name, folder=self),
                  dictionary=dictionary,
                  xml_data=xml_data,
                  binary_data=binary_data,
              )
              return CreateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

          @require_id
          def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
              user_configuration = UserConfiguration(
                  user_configuration_name=UserConfigurationName(name=name, folder=self),
                  dictionary=dictionary,
                  xml_data=xml_data,
                  binary_data=binary_data,
              )
              return UpdateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

          @require_id
          def delete_user_configuration(self, name):
              return DeleteUserConfiguration(account=self.account).get(
                  user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
              )

          @require_id
          def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
              """Create a pull subscription.

              :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES
              :param watermark: An event bookmark as returned by some sync services
              :param timeout: Timeout of the subscription, in minutes. Timeout is reset when the server receives a
              GetEvents request for this subscription.
              :return: The subscription ID and a watermark
              """
              s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
                  event_types=event_types, watermark=watermark, timeout=timeout,
              ))
              if len(s_ids) != 1:
                  raise ValueError('Expected result length 1, but got %s' % s_ids)
              s_id = s_ids[0]
              if isinstance(s_id, Exception):
                  raise s_id
              return s_id

          @require_id
          def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                                status_frequency=1):
              """Create a push subscription.

              :param callback_url: A client-defined URL that the server will call
              :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
              :param watermark: An event bookmark as returned by some sync services
              :param status_frequency: The frequency, in minutes, that the callback URL will be called with.
              :return: The subscription ID and a watermark
              """
              s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
                  event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
              ))
              if len(s_ids) != 1:
                  raise ValueError('Expected result length 1, but got %s' % s_ids)
              s_id = s_ids[0]
              if isinstance(s_id, Exception):
                  raise s_id
              return s_id

          @require_id
          def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
              """Create a streaming subscription.

              :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
              :return: The subscription ID
              """
              s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
                  event_types=event_types,
              ))
              if len(s_ids) != 1:
                  raise ValueError('Expected result length 1, but got %s' % s_ids)
              s_id = s_ids[0]
              if isinstance(s_id, Exception):
                  raise s_id
              return s_id

          @require_id
          def pull_subscription(self, **kwargs):
              return PullSubscription(folder=self, **kwargs)

          @require_id
          def push_subscription(self, **kwargs):
              return PushSubscription(folder=self, **kwargs)

          @require_id
          def streaming_subscription(self, **kwargs):
              return StreamingSubscription(folder=self, **kwargs)

          def unsubscribe(self, subscription_id):
              """Unsubscribe. Only applies to pull and streaming notifications.

              :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]()
              :return: True

              This method doesn't need the current folder instance, but it makes sense to keep the method along the other
              sync methods.
              """
              return Unsubscribe(account=self.account).get(subscription_id=subscription_id)

          def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
              """Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
              this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

              :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
              :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
              :param ignore: A list of Item IDs to ignore in the sync
              :param max_changes_returned: The max number of change
              :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
                 values are specified in SyncFolderitems.SYNC_SCOPES
              :return: A generator of (change_type, item) tuples
              """
              if not sync_state:
                  sync_state = self.item_sync_state
              try:
                  yield from FolderCollection(account=self.account, folders=[self]).sync_items(
                      sync_state=sync_state,
                      only_fields=only_fields,
                      ignore=ignore,
                      max_changes_returned=max_changes_returned,
                      sync_scope=sync_scope,
                  )
              except SyncCompleted as e:
                  # Set the new sync state on the folder instance
                  self.item_sync_state = e.sync_state

          def sync_hierarchy(self, sync_state=None, only_fields=None):
              """Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
              changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
              sync state.

              :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
              :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
              :return:
              """
              if not sync_state:
                  sync_state = self.folder_sync_state
              try:
                  yield from FolderCollection(account=self.account, folders=[self]).sync_hierarchy(
                      sync_state=sync_state,
                      only_fields=only_fields,
                  )
              except SyncCompleted as e:
                  # Set the new sync state on the folder instance
                  self.folder_sync_state = e.sync_state

          def get_events(self, subscription_id, watermark):
              """Get events since the given watermark. Non-blocking.

              :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|push]()
              :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call.
              :return: A Notification object containing a list of events

              This method doesn't need the current folder instance, but it makes sense to keep the method along the other
              sync methods.
              """
              svc = GetEvents(account=self.account)
              while True:
                  notification = svc.get(subscription_id=subscription_id, watermark=watermark)
                  yield notification
                  if not notification.more_events:
                      break

          def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
              """Get events since the subscription was created, in streaming mode. This method will block as many minutes
              as specified by 'connection_timeout'.

              :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
              :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
              is reached.
              :param max_notifications_returned: If specified, will exit after receiving this number of notifications
              :return: A generator of Notification objects, each containing a list of events

              This method doesn't need the current folder instance, but it makes sense to keep the method along the other
              sync methods.
              """
              # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
              request_timeout = connection_timeout*60 + 60
              svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
              for i, notification in enumerate(
                      svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
                      start=1
              ):
                  yield notification
                  if max_notifications_returned and i >= max_notifications_returned:
                      svc.stop_streaming()
                      break
              if svc.error_subscription_ids:
                  raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)

          def __floordiv__(self, other):
              """Support the some_folder // 'child_folder' // 'child_of_child_folder' navigation syntax.

              Works like as __truediv__ but does not touch the folder cache.

              This is useful if the folder hierarchy contains a huge number of folders and you don't want to fetch them all

              :param other:
              :return:
              """
              if other == '..':
                  raise ValueError('Cannot get parent without a folder cache')

              if other == '.':
                  return self

              # Assume an exact match on the folder name in a shallow search will only return at most one folder
              try:
                  return SingleFolderQuerySet(account=self.account, folder=self).depth(SHALLOW_FOLDERS).get(name=other)
              except DoesNotExist:
                  raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

          def __truediv__(self, other):
              """Support the some_folder / 'child_folder' / 'child_of_child_folder' navigation syntax."""
              if other == '..':
                  if not self.parent:
                      raise ValueError('Already at top')
                  return self.parent
              if other == '.':
                  return self
              for c in self.children:
                  if c.name == other:
                      return c
              raise ErrorFolderNotFound("No subfolder with name '%s'" % other)

          def __repr__(self):
              return self.__class__.__name__ + \
                     repr((self.root, self.name, self.total_count, self.unread_count, self.child_folder_count,
                           self.folder_class, self.id, self.changekey))

          def __str__(self):
              return '%s (%s)' % (self.__class__.__name__, self.name)

  ### Ancestors

  - RegisterMixIn
  - IdChangeKeyMixIn
  - EWSElement
  - SearchableMixIn

  ### Subclasses

  - Folder
  - RootOfHierarchy

  ### Class variables

  `var CONTAINER_CLASS`
  `var DEFAULT_FOLDER_TRAVERSAL_DEPTH`
  `var DEFAULT_ITEM_TRAVERSAL_DEPTH`
  `var DISTINGUISHED_FOLDER_ID`
  `var ELEMENT_NAME`
  `var FIELDS`
  `var ID_ELEMENT_CLS`

    MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folderid

  `var INSERT_AFTER_FIELD`
  `var ITEM_MODEL_MAP`
  `var LOCALIZED_NAMES`
  `var NAMESPACE`
  `var get_folder_allowed`
  `var supported_from`
  `var supported_item_models`

  ### Static methods

  `def allowed_item_fields(version)`
    Expand source code

        @classmethod
        def allowed_item_fields(cls, version):
            # Return non-ID fields of all item classes allowed in this folder type
            fields = set()
            for item_model in cls.supported_item_models:
                fields.update(
                    set(item_model.supported_fields(version=version))
                )
            return fields

  `def folder_cls_from_container_class(container_class)`

    Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS because many folder classes have the same CONTAINER_CLASS.

    :param container_class: :return:

    Expand source code

        @staticmethod
        def folder_cls_from_container_class(container_class):
            """Return a reasonable folder class given a container class, e.g. 'IPF.Note'. Don't iterate WELLKNOWN_FOLDERS
            because many folder classes have the same CONTAINER_CLASS.

            :param container_class:
            :return:
            """
            from .known_folders import Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, \
                RecipientCache, RSSFeeds
            for folder_cls in (
                    Messages, Tasks, Calendar, ConversationSettings, Contacts, GALContacts, Reminders, RecipientCache,
                    RSSFeeds):
                if folder_cls.CONTAINER_CLASS == container_class:
                    return folder_cls
            raise KeyError()

  `def get_item_field_by_fieldname(fieldname)`
    Expand source code

        @classmethod
        def get_item_field_by_fieldname(cls, fieldname):
            for item_model in cls.supported_item_models:
                try:
                    return item_model.get_field_by_fieldname(fieldname)
                except InvalidField:
                    pass
            raise InvalidField("%r is not a valid field name on %s" % (fieldname, cls.supported_item_models))

  `def item_model_from_tag(tag)`
    Expand source code

        @classmethod
        def item_model_from_tag(cls, tag):
            try:
                return cls.ITEM_MODEL_MAP[tag]
            except KeyError:
                raise ValueError('Item type %s was unexpected in a %s folder' % (tag, cls.__name__))

  `def localized_names(locale)`
    Expand source code

        @classmethod
        def localized_names(cls, locale):
            # Return localized names for a specific locale. If no locale-specific names exist, return the default names,
            # if any.
            return tuple(s.lower() for s in cls.LOCALIZED_NAMES.get(locale, cls.LOCALIZED_NAMES.get(None, [])))

  `def resolve(account, folder)`
    Expand source code

        @classmethod
        def resolve(cls, account, folder):
            # Resolve a single folder
            folders = list(FolderCollection(account=account, folders=[folder]).resolve())
            if not folders:
                raise ErrorFolderNotFound('Could not find folder %r' % folder)
            if len(folders) != 1:
                raise ValueError('Expected result length 1, but got %s' % folders)
            f = folders[0]
            if isinstance(f, Exception):
                raise f
            if f.__class__ != cls:
                raise ValueError("Expected folder %r to be a %s instance" % (f, cls))
            return f

  `def supports_version(version)`
    Expand source code

        @classmethod
        def supports_version(cls, version):
            # 'version' is a Version instance, for convenience by callers
            if not isinstance(version, Version):
                raise ValueError("'version' %r must be a Version instance" % version)
            if not cls.supported_from:
                return True
            return version.build >= cls.supported_from

  ### Instance variables

  `var absolute`
    Expand source code

        @property
        def absolute(self):
            return ''.join('/%s' % p.name for p in self.parts)

  `var account`
    Expand source code

        @property
        @abc.abstractmethod
        def account(self):
            pass

  `var child_folder_count`
  `var children`
    Expand source code

        @property
        def children(self):
            # It's dangerous to return a generator here because we may then call methods on a child that result in the
            # cache being updated while it's iterated.
            return FolderCollection(account=self.account, folders=self.root.get_children(self))

  `var folder_class`
  `var folder_sync_state`

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

  `var has_distinguished_name`
    Expand source code

        @property
        def has_distinguished_name(self):
            return self.name and self.DISTINGUISHED_FOLDER_ID and self.name.lower() == self.DISTINGUISHED_FOLDER_ID.lower()

  `var is_deletable`
    Expand source code

        @property
        def is_deletable(self):
            return not self.is_distinguished

  `var is_distinguished`

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

  `var item_sync_state`

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

  `var name`
  `var parent`
    Expand source code

        @property
        @abc.abstractmethod
        def parent(self):
            pass

  `var parent_folder_id`
  `var parts`
    Expand source code

        @property
        def parts(self):
            parts = [self]
            f = self.parent
            while f:
                parts.insert(0, f)
                f = f.parent
            return parts

  `var root`
    Expand source code

        @property
        @abc.abstractmethod
        def root(self):
            pass

  `var total_count`
  `var unread_count`

  ### Methods

  `def all(self)`
    Expand source code

        def all(self):
            return FolderCollection(account=self.account, folders=[self]).all()

  `def bulk_create(self, items, *args, **kwargs)`
    Expand source code

        def bulk_create(self, items, *args, **kwargs):
            return self.account.bulk_create(folder=self, items=items, *args, **kwargs)

  `def clean(self, version=None)`
    Expand source code

        def clean(self, version=None):
            super().clean(version=version)
            # Set a default folder class for new folders. A folder class cannot be changed after saving.
            if self.id is None and self.folder_class is None:
                self.folder_class = self.CONTAINER_CLASS

  `def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None)`
    Expand source code

        @require_id
        def create_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
            user_configuration = UserConfiguration(
                user_configuration_name=UserConfigurationName(name=name, folder=self),
                dictionary=dictionary,
                xml_data=xml_data,
                binary_data=binary_data,
            )
            return CreateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

  `def delete(self, delete_type='HardDelete')`
    Expand source code

        def delete(self, delete_type=HARD_DELETE):
            if delete_type not in DELETE_TYPE_CHOICES:
                raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
            DeleteFolder(account=self.account).get(folders=[self], delete_type=delete_type)
            self.root.remove_folder(self)  # Remove the updated folder from the cache
            self._id = None

  `def delete_user_configuration(self, name)`
    Expand source code

        @require_id
        def delete_user_configuration(self, name):
            return DeleteUserConfiguration(account=self.account).get(
                user_configuration_name=UserConfigurationNameMNS(name=name, folder=self)
            )

  `def empty(self, delete_type='HardDelete', delete_sub_folders=False)`
    Expand source code

        def empty(self, delete_type=HARD_DELETE, delete_sub_folders=False):
            if delete_type not in DELETE_TYPE_CHOICES:
                raise ValueError("'delete_type' %s must be one of %s" % (delete_type, DELETE_TYPE_CHOICES))
            EmptyFolder(account=self.account).get(
                folders=[self], delete_type=delete_type, delete_sub_folders=delete_sub_folders
            )
            if delete_sub_folders:
                # We don't know exactly what was deleted, so invalidate the entire folder cache to be safe
                self.root.clear_cache()

  `def exclude(self, *args, **kwargs)`
    Expand source code

        def exclude(self, *args, **kwargs):
            return FolderCollection(account=self.account, folders=[self]).exclude(*args, **kwargs)

  `def filter(self, *args, **kwargs)`
    Expand source code

        def filter(self, *args, **kwargs):
            return FolderCollection(account=self.account, folders=[self]).filter(*args, **kwargs)

  `def get(self, *args, **kwargs)`
    Expand source code

        def get(self, *args, **kwargs):
            return FolderCollection(account=self.account, folders=[self]).get(*args, **kwargs)

  `def get_events(self, subscription_id, watermark)`

    Get events since the given watermark. Non-blocking.

    :param subscription_id: A subscription ID as acquired by .subscribe_to_pull|push :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call. :return: A Notification object containing a list of events

    This method doesn't need the current folder instance, but it makes sense to keep the method along the other sync methods.

    Expand source code

        def get_events(self, subscription_id, watermark):
            """Get events since the given watermark. Non-blocking.

            :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|push]()
            :param watermark: Either the watermark from the subscription, or as returned in the last .get_events() call.
            :return: A Notification object containing a list of events

            This method doesn't need the current folder instance, but it makes sense to keep the method along the other
            sync methods.
            """
            svc = GetEvents(account=self.account)
            while True:
                notification = svc.get(subscription_id=subscription_id, watermark=watermark)
                yield notification
                if not notification.more_events:
                    break

  `def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None)`

    Get events since the subscription was created, in streaming mode. This method will block as many minutes as specified by 'connection_timeout'.

    :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming() :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout is reached. :param max_notifications_returned: If specified, will exit after receiving this number of notifications :return: A generator of Notification objects, each containing a list of events

    This method doesn't need the current folder instance, but it makes sense to keep the method along the other sync methods.

    Expand source code

        def get_streaming_events(self, subscription_id, connection_timeout=1, max_notifications_returned=None):
            """Get events since the subscription was created, in streaming mode. This method will block as many minutes
            as specified by 'connection_timeout'.

            :param subscription_id: A subscription ID as acquired by .subscribe_to_streaming()
            :param connection_timeout: Timeout of the connection, in minutes. The connection is closed after this timeout
            is reached.
            :param max_notifications_returned: If specified, will exit after receiving this number of notifications
            :return: A generator of Notification objects, each containing a list of events

            This method doesn't need the current folder instance, but it makes sense to keep the method along the other
            sync methods.
            """
            # Add 60 seconds to the timeout, to allow us to always get the final message containing ConnectionStatus=Closed
            request_timeout = connection_timeout*60 + 60
            svc = GetStreamingEvents(account=self.account, timeout=request_timeout)
            for i, notification in enumerate(
                    svc.call(subscription_ids=[subscription_id], connection_timeout=connection_timeout),
                    start=1
            ):
                yield notification
                if max_notifications_returned and i >= max_notifications_returned:
                    svc.stop_streaming()
                    break
            if svc.error_subscription_ids:
                raise ErrorInvalidSubscription('Invalid subscription IDs: %s' % svc.error_subscription_ids)

  `def get_user_configuration(self, name, properties='All')`
    Expand source code

        @require_id
        def get_user_configuration(self, name, properties=ALL):
            return GetUserConfiguration(account=self.account).get(
                user_configuration_name=UserConfigurationNameMNS(name=name, folder=self),
                properties=properties,
            )

  `def glob(self, pattern)`
    Expand source code

        def glob(self, pattern):
            return FolderCollection(account=self.account, folders=self._glob(pattern))

  `def move(self, to_folder)`
    Expand source code

        def move(self, to_folder):
            res = MoveFolder(account=self.account).get(folders=[self], to_folder=to_folder)
            folder_id, changekey = res.id, res.changekey
            if self.id != folder_id:
                raise ValueError('ID mismatch')
            # Don't check changekey value. It may not change on no-op moves
            self.changekey = changekey
            self.parent_folder_id = ParentFolderId(id=to_folder.id, changekey=to_folder.changekey)
            self.root.update_folder(self)  # Update the folder in the cache

  `def none(self)`
    Expand source code

        def none(self):
            return FolderCollection(account=self.account, folders=[self]).none()

  `def normalize_fields(self, fields)`
    Expand source code

        def normalize_fields(self, fields):
            # Takes a list of fieldnames, Field or FieldPath objects pointing to item fields. Turns them into FieldPath
            # objects and adds internal timezone fields if necessary. Assume fields are already validated.
            fields = list(fields)
            has_start, has_end = False, False
            for i, field_path in enumerate(fields):
                # Allow both Field and FieldPath instances and string field paths as input
                if isinstance(field_path, str):
                    field_path = FieldPath.from_string(field_path=field_path, folder=self)
                    fields[i] = field_path
                elif isinstance(field_path, Field):
                    field_path = FieldPath(field=field_path)
                    fields[i] = field_path
                if not isinstance(field_path, FieldPath):
                    raise ValueError("Field %r must be a string or FieldPath instance" % field_path)
                if field_path.field.name == 'start':
                    has_start = True
                elif field_path.field.name == 'end':
                    has_end = True

            # For CalendarItem items, we want to inject internal timezone fields. See also CalendarItem.clean()
            if CalendarItem in self.supported_item_models:
                meeting_tz_field, start_tz_field, end_tz_field = CalendarItem.timezone_fields()
                if self.account.version.build < EXCHANGE_2010:
                    if has_start or has_end:
                        fields.append(FieldPath(field=meeting_tz_field))
                else:
                    if has_start:
                        fields.append(FieldPath(field=start_tz_field))
                    if has_end:
                        fields.append(FieldPath(field=end_tz_field))
            return fields

  `def people(self)`
    Expand source code

        def people(self):
            # No point in using a FolderCollection because FindPeople only supports one folder
            return FolderCollection(account=self.account, folders=[self]).people()

  `def pull_subscription(self, **kwargs)`
    Expand source code

        @require_id
        def pull_subscription(self, **kwargs):
            return PullSubscription(folder=self, **kwargs)

  `def push_subscription(self, **kwargs)`
    Expand source code

        @require_id
        def push_subscription(self, **kwargs):
            return PushSubscription(folder=self, **kwargs)

  `def refresh(self)`
    Expand source code

        @require_id
        def refresh(self):
            fresh_folder = self.resolve(account=self.account, folder=self)
            if self.id != fresh_folder.id:
                raise ValueError('ID mismatch')
            # Apparently, the changekey may get updated
            for f in self.FIELDS:
                setattr(self, f.name, getattr(fresh_folder, f.name))
            return self

  `def save(self, update_fields=None)`
    Expand source code

        def save(self, update_fields=None):
            if self.id is None:
                # New folder
                if update_fields:
                    raise ValueError("'update_fields' is only valid for updates")
                res = CreateFolder(account=self.account).get(parent_folder=self.parent, folders=[self])
                self._id = self.ID_ELEMENT_CLS(res.id, res.changekey)
                self.root.add_folder(self)  # Add this folder to the cache
                return self

            # Update folder
            if not update_fields:
                # The fields to update was not specified explicitly. Update all fields where update is possible
                update_fields = []
                for f in self.supported_fields(version=self.account.version):
                    if f.is_read_only:
                        # These cannot be changed
                        continue
                    if (f.is_required or f.is_required_after_save) and (
                            getattr(self, f.name) is None or (f.is_list and not getattr(self, f.name))
                    ):
                        # These are required and cannot be deleted
                        continue
                    update_fields.append(f.name)
            res = UpdateFolder(account=self.account).get(folders=[(self, update_fields)])
            folder_id, changekey = res.id, res.changekey
            if self.id != folder_id:
                raise ValueError('ID mismatch')
            # Don't check changekey value. It may not change on no-op updates
            self.changekey = changekey
            self.root.update_folder(self)  # Update the folder in the cache
            return self

  `def streaming_subscription(self, **kwargs)`
    Expand source code

        @require_id
        def streaming_subscription(self, **kwargs):
            return StreamingSubscription(folder=self, **kwargs)

  `def subscribe_to_pull(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, timeout=60)`

    Create a pull subscription.

    :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES :param watermark: An event bookmark as returned by some sync services :param timeout: Timeout of the subscription, in minutes. Timeout is reset when the server receives a GetEvents request for this subscription. :return: The subscription ID and a watermark

    Expand source code

        @require_id
        def subscribe_to_pull(self, event_types=SubscribeToPull.EVENT_TYPES, watermark=None, timeout=60):
            """Create a pull subscription.

            :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPull.EVENT_TYPES
            :param watermark: An event bookmark as returned by some sync services
            :param timeout: Timeout of the subscription, in minutes. Timeout is reset when the server receives a
            GetEvents request for this subscription.
            :return: The subscription ID and a watermark
            """
            s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_pull(
                event_types=event_types, watermark=watermark, timeout=timeout,
            ))
            if len(s_ids) != 1:
                raise ValueError('Expected result length 1, but got %s' % s_ids)
            s_id = s_ids[0]
            if isinstance(s_id, Exception):
                raise s_id
            return s_id

  `def subscribe_to_push(self, callback_url, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'), watermark=None, status_frequency=1)`

    Create a push subscription.

    :param callback_url: A client-defined URL that the server will call :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES :param watermark: An event bookmark as returned by some sync services :param status_frequency: The frequency, in minutes, that the callback URL will be called with. :return: The subscription ID and a watermark

    Expand source code

        @require_id
        def subscribe_to_push(self, callback_url, event_types=SubscribeToPush.EVENT_TYPES, watermark=None,
                              status_frequency=1):
            """Create a push subscription.

            :param callback_url: A client-defined URL that the server will call
            :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
            :param watermark: An event bookmark as returned by some sync services
            :param status_frequency: The frequency, in minutes, that the callback URL will be called with.
            :return: The subscription ID and a watermark
            """
            s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_push(
                event_types=event_types, watermark=watermark, status_frequency=status_frequency, callback_url=callback_url,
            ))
            if len(s_ids) != 1:
                raise ValueError('Expected result length 1, but got %s' % s_ids)
            s_id = s_ids[0]
            if isinstance(s_id, Exception):
                raise s_id
            return s_id

  `def subscribe_to_streaming(self, event_types=('CopiedEvent', 'CreatedEvent', 'DeletedEvent', 'ModifiedEvent', 'MovedEvent', 'NewMailEvent', 'FreeBusyChangedEvent'))`

    Create a streaming subscription.

    :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES :return: The subscription ID

    Expand source code

        @require_id
        def subscribe_to_streaming(self, event_types=SubscribeToPush.EVENT_TYPES):
            """Create a streaming subscription.

            :param event_types: List of event types to subscribe to. Possible values defined in SubscribeToPush.EVENT_TYPES
            :return: The subscription ID
            """
            s_ids = list(FolderCollection(account=self.account, folders=[self]).subscribe_to_streaming(
                event_types=event_types,
            ))
            if len(s_ids) != 1:
                raise ValueError('Expected result length 1, but got %s' % s_ids)
            s_id = s_ids[0]
            if isinstance(s_id, Exception):
                raise s_id
            return s_id

  `def sync_hierarchy(self, sync_state=None, only_fields=None)`

    Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new sync state.

    :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service. :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields :return:

    Expand source code

        def sync_hierarchy(self, sync_state=None, only_fields=None):
            """Return all folder changes to a folder hierarchy, as a generator. If sync_state is specified, get all folder
            changes after this sync state. After fully consuming the generator, self.folder_sync_state will hold the new
            sync state.

            :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
            :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
            :return:
            """
            if not sync_state:
                sync_state = self.folder_sync_state
            try:
                yield from FolderCollection(account=self.account, folders=[self]).sync_hierarchy(
                    sync_state=sync_state,
                    only_fields=only_fields,
                )
            except SyncCompleted as e:
                # Set the new sync state on the folder instance
                self.folder_sync_state = e.sync_state

  `def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None)`

    Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

    :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service. :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields :param ignore: A list of Item IDs to ignore in the sync :param max_changes_returned: The max number of change :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible values are specified in SyncFolderitems.SYNC_SCOPES :return: A generator of (change_type, item) tuples

    Expand source code

        def sync_items(self, sync_state=None, only_fields=None, ignore=None, max_changes_returned=None, sync_scope=None):
            """Return all item changes to a folder, as a generator. If sync_state is specified, get all item changes after
            this sync state. After fully consuming the generator, self.item_sync_state will hold the new sync state.

            :param sync_state: The state of the sync. Returned by a successful call to the SyncFolderitems service.
            :param only_fields: A list of string or FieldPath items specifying the fields to fetch. Default to all fields
            :param ignore: A list of Item IDs to ignore in the sync
            :param max_changes_returned: The max number of change
            :param sync_scope: Specify whether to return just items, or items and folder associated information. Possible
               values are specified in SyncFolderitems.SYNC_SCOPES
            :return: A generator of (change_type, item) tuples
            """
            if not sync_state:
                sync_state = self.item_sync_state
            try:
                yield from FolderCollection(account=self.account, folders=[self]).sync_items(
                    sync_state=sync_state,
                    only_fields=only_fields,
                    ignore=ignore,
                    max_changes_returned=max_changes_returned,
                    sync_scope=sync_scope,
                )
            except SyncCompleted as e:
                # Set the new sync state on the folder instance
                self.item_sync_state = e.sync_state

  `def test_access(self)`

    Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the service user doesn't have access to the calendar. This will throw the most common errors.

    Expand source code

        def test_access(self):
            """Does a simple FindItem to test (read) access to the folder. Maybe the account doesn't exist, maybe the
            service user doesn't have access to the calendar. This will throw the most common errors.
            """
            self.all().exists()
            return True

  `def to_folder_id(self)`
    Expand source code

        def to_folder_id(self):
            if self.is_distinguished:
                # Don't add the changekey here. When modifying folder content, we usually don't care if others have changed
                # the folder content since we fetched the changekey.
                if self.account:
                    return DistinguishedFolderId(
                        id=self.DISTINGUISHED_FOLDER_ID,
                        mailbox=Mailbox(email_address=self.account.primary_smtp_address)
                    )
                return DistinguishedFolderId(id=self.DISTINGUISHED_FOLDER_ID)
            if self.id:
                return FolderId(id=self.id, changekey=self.changekey)
            raise ValueError('Must be a distinguished folder or have an ID')

  `def to_id_xml(self, version)`
    Expand source code

        def to_id_xml(self, version):
            # Folder(name='Foo') is a perfectly valid ID to e.g. create a folder
            return self.to_xml(version=version)

  `def to_xml(self, version)`
    Expand source code

        def to_xml(self, version):
            try:
                return self.to_folder_id().to_xml(version=version)
            except ValueError:
                return super().to_xml(version=version)

  `def tree(self)`

    Return a string representation of the folder structure of this folder. Example:

    root ├── inbox │ └── todos └── archive ├── Last Job ├── exchangelib issues └── Mom

    Expand source code

        def tree(self):
            """Return a string representation of the folder structure of this folder. Example:

            root
            ├── inbox
            │   └── todos
            └── archive
                ├── Last Job
                ├── exchangelib issues
                └── Mom
            """
            tree = '%s\n' % self.name
            children = list(self.children)
            for i, c in enumerate(sorted(children, key=attrgetter('name')), start=1):
                nodes = c.tree().split('\n')
                for j, node in enumerate(nodes, start=1):
                    if i != len(children) and j == 1:
                        # Not the last child, but the first node, which is the name of the child
                        tree += '├── %s\n' % node
                    elif i != len(children) and j > 1:
                        # Not the last child, and not name of child
                        tree += '│   %s\n' % node
                    elif i == len(children) and j == 1:
                        # Not the last child, but the first node, which is the name of the child
                        tree += '└── %s\n' % node
                    else:  # Last child, and not name of child
                        tree += '    %s\n' % node
            return tree.strip()

  `def unsubscribe(self, subscription_id)`

    Unsubscribe. Only applies to pull and streaming notifications.

    :param subscription_id: A subscription ID as acquired by .subscribe_to_pull|streaming :return: True

    This method doesn't need the current folder instance, but it makes sense to keep the method along the other sync methods.

    Expand source code

        def unsubscribe(self, subscription_id):
            """Unsubscribe. Only applies to pull and streaming notifications.

            :param subscription_id: A subscription ID as acquired by .subscribe_to_[pull|streaming]()
            :return: True

            This method doesn't need the current folder instance, but it makes sense to keep the method along the other
            sync methods.
            """
            return Unsubscribe(account=self.account).get(subscription_id=subscription_id)

  `def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None)`
    Expand source code

        @require_id
        def update_user_configuration(self, name, dictionary=None, xml_data=None, binary_data=None):
            user_configuration = UserConfiguration(
                user_configuration_name=UserConfigurationName(name=name, folder=self),
                dictionary=dictionary,
                xml_data=xml_data,
                binary_data=binary_data,
            )
            return UpdateUserConfiguration(account=self.account).get(user_configuration=user_configuration)

  `def validate_item_field(self, field, version)`
    Expand source code

        def validate_item_field(self, field, version):
            # Takes a fieldname, Field or FieldPath object pointing to an item field, and checks that it is valid
            # for the item types supported by this folder.

            # For each field, check if the field is valid for any of the item models supported by this folder
            for item_model in self.supported_item_models:
                try:
                    item_model.validate_field(field=field, version=version)
                    break
                except InvalidField:
                    continue
            else:
                raise InvalidField("%r is not a valid field on %s" % (field, self.supported_item_models))

  `def walk(self)`
    Expand source code

        def walk(self):
            return FolderCollection(account=self.account, folders=self._walk())

  `def wipe(self, page_size=None)`
    Expand source code

        def wipe(self, page_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!
            _seen = _seen or set()
            if self.id in _seen:
                raise RecursionError('We already tried to wipe %s' % self)
            if _level > 16:
                raise RecursionError('Max recursion level reached: %s' % _level)
            _seen.add(self.id)
            log.warning('Wiping %s', self)
            has_distinguished_subfolders = any(f.is_distinguished for f in self.children)
            try:
                if has_distinguished_subfolders:
                    self.empty(delete_sub_folders=False)
                else:
                    self.empty(delete_sub_folders=True)
            except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
                try:
                    if has_distinguished_subfolders:
                        raise  # We already tried this
                    self.empty(delete_sub_folders=False)
                except (ErrorAccessDenied, ErrorCannotEmptyFolder, ErrorItemNotFound):
                    log.warning('Not allowed to empty %s. Trying to delete items instead', self)
                    try:
                        self.all().delete(**dict(page_size=page_size) if page_size else {})
                    except (ErrorAccessDenied, ErrorCannotDeleteObject, ErrorItemNotFound):
                        log.warning('Not allowed to delete items in %s', self)
            _level += 1
            for f in self.children:
                f.wipe(page_size=page_size, _seen=_seen, _level=_level)
                # Remove non-distinguished children that are empty and have no subfolders
                if f.is_deletable and not f.children:
                    log.warning('Deleting folder %s', f)
                    try:
                        f.delete()
                    except ErrorDeleteDistinguishedFolder:
                        log.warning('Tried to delete a distinguished folder (%s)', f)

  ### Inherited members

  - `RegisterMixIn`:
    - `add_field`
    - `deregister`
    - `register`
    - `remove_field`
    - `supported_fields`
    - `validate_field`
`class BaseSubscription (folder, **subscription_kwargs)`
  Expand source code

      class BaseSubscription(metaclass=abc.ABCMeta):
          def __init__(self, folder, **subscription_kwargs):
              self.folder = folder
              self.subscription_kwargs = subscription_kwargs
              self.subscription_id = None

          def __enter__(self):
              pass

          def __exit__(self, *args, **kwargs):
              self.folder.unsubscribe(subscription_id=self.subscription_id)
              self.subscription_id = None

  ### Subclasses

  - PullSubscription
  - PushSubscription
  - StreamingSubscription
`class Folder (**kwargs)`

  MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folder

  Expand source code

      class Folder(BaseFolder):
          """MSDN: https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/folder"""

          permission_set = PermissionSetField(field_uri='folder:PermissionSet', supported_from=EXCHANGE_2007_SP1)
          effective_rights = EffectiveRightsField(field_uri='folder:EffectiveRights', is_read_only=True,
                                                  supported_from=EXCHANGE_2007_SP1)

          __slots__ = '_root',

          def __init__(self, **kwargs):
              self._root = kwargs.pop('root', None)  # This is a pointer to the root of the folder hierarchy
              parent = kwargs.pop('parent', None)
              if parent:
                  if self.root:
                      if parent.root != self.root:
                          raise ValueError("'parent.root' must match 'root'")
                  else:
                      self.root = parent.root
                  if 'parent_folder_id' in kwargs and parent.id != kwargs['parent_folder_id']:
                      raise ValueError("'parent_folder_id' must match 'parent' ID")
                  kwargs['parent_folder_id'] = ParentFolderId(id=parent.id, changekey=parent.changekey)
              super().__init__(**kwargs)

          @property
          def account(self):
              if self.root is None:
                  return None
              return self.root.account

          @property
          def root(self):
              return self._root

          @root.setter
          def root(self, value):
              self._root = value

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

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

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

              :param root:
              :return:
              """
              try:
                  return cls.resolve(
                      account=root.account,
                      folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
                  )
              except MISSING_FOLDER_ERRORS:
                  raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

          @property
          def parent(self):
              if not self.parent_folder_id:
                  return None
              if self.parent_folder_id.id == self.id:
                  # Some folders have a parent that references itself. Avoid circular references here
                  return None
              return self.root.get_folder(self.parent_folder_id)

          @parent.setter
          def parent(self, value):
              if value is None:
                  self.parent_folder_id = None
              else:
                  if not isinstance(value, BaseFolder):
                      raise ValueError("'value' %r must be a Folder instance" % value)
                  self.root = value.root
                  self.parent_folder_id = ParentFolderId(id=value.id, changekey=value.changekey)

          def clean(self, version=None):
              from .roots import RootOfHierarchy
              super().clean(version=version)
              if self.root and not isinstance(self.root, RootOfHierarchy):
                  raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

          @classmethod
          def from_xml_with_root(cls, elem, root):
              folder = cls.from_xml(elem=elem, account=root.account)
              folder_cls = cls
              if cls == Folder:
                  # We were called on the generic Folder class. Try to find a more specific class to return objects as.
                  #
                  # The "FolderClass" element value is the only indication we have in the FindFolder response of which
                  # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
                  # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
                  # folders types for e.g. Account.inbox and Account.trash.
                  #
                  # We should be able to just use the name, but apparently default folder names can be renamed to a set of
                  # localized names using a PowerShell command:
                  # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
                  #
                  # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
                  # folder class by the "FolderClass" value.
                  #
                  # The returned XML may contain neither folder class nor name. In that case, we default to the generic
                  # Folder class.
                  if folder.name:
                      try:
                          # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                          folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                                        locale=root.account.locale)
                          log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
                      except KeyError:
                          pass
                  if folder.folder_class and folder_cls == Folder:
                      try:
                          folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                          log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                                    folder.name)
                      except KeyError:
                          pass
                  if folder_cls == Folder:
                      log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
              return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})

  ### Ancestors

  - BaseFolder
  - RegisterMixIn
  - IdChangeKeyMixIn
  - EWSElement
  - SearchableMixIn

  ### Subclasses

  - AllItems
  - Audits
  - Calendar
  - CalendarLogging
  - CommonViews
  - Contacts
  - ConversationSettings
  - DefaultFoldersChangeHistory
  - DeferredAction
  - DeletedItems
  - ExchangeSyncData
  - Files
  - FreebusyData
  - GraphAnalytics
  - Location
  - MailboxAssociations
  - Messages
  - ParkedMessages
  - PassThroughSearchResults
  - PdpProfileV2Secured
  - RSSFeeds
  - Reminders
  - Schedule
  - Sharing
  - Shortcuts
  - Signal
  - SmsAndChatsSync
  - SpoolerQueue
  - System
  - System1
  - Tasks
  - TemporarySaves
  - Views
  - WellknownFolder
  - WorkingSet

  ### Class variables

  `var FIELDS`

  ### Static methods

  `def from_xml_with_root(elem, root)`
    Expand source code

        @classmethod
        def from_xml_with_root(cls, elem, root):
            folder = cls.from_xml(elem=elem, account=root.account)
            folder_cls = cls
            if cls == Folder:
                # We were called on the generic Folder class. Try to find a more specific class to return objects as.
                #
                # The "FolderClass" element value is the only indication we have in the FindFolder response of which
                # folder class we should create the folder with. And many folders share the same 'FolderClass' value, e.g.
                # Inbox and DeletedItems. We want to distinguish between these because otherwise we can't locate the right
                # folders types for e.g. Account.inbox and Account.trash.
                #
                # We should be able to just use the name, but apparently default folder names can be renamed to a set of
                # localized names using a PowerShell command:
                # https://docs.microsoft.com/en-us/powershell/module/exchange/client-access/Set-MailboxRegionalConfiguration
                #
                # Instead, search for a folder class using the localized name. If none are found, fall back to getting the
                # folder class by the "FolderClass" value.
                #
                # The returned XML may contain neither folder class nor name. In that case, we default to the generic
                # Folder class.
                if folder.name:
                    try:
                        # TODO: fld_class.LOCALIZED_NAMES is most definitely neither complete nor authoritative
                        folder_cls = root.folder_cls_from_folder_name(folder_name=folder.name,
                                                                      locale=root.account.locale)
                        log.debug('Folder class %s matches localized folder name %s', folder_cls, folder.name)
                    except KeyError:
                        pass
                if folder.folder_class and folder_cls == Folder:
                    try:
                        folder_cls = cls.folder_cls_from_container_class(container_class=folder.folder_class)
                        log.debug('Folder class %s matches container class %s (%s)', folder_cls, folder.folder_class,
                                  folder.name)
                    except KeyError:
                        pass
                if folder_cls == Folder:
                    log.debug('Fallback to class Folder (folder_class %s, name %s)', folder.folder_class, folder.name)
            return folder_cls(root=root, **{f.name: getattr(folder, f.name) for f in folder.FIELDS})

  `def get_distinguished(root)`

    Get the distinguished folder for this folder class.

    :param root: :return:

    Expand source code

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

            :param root:
            :return:
            """
            try:
                return cls.resolve(
                    account=root.account,
                    folder=cls(root=root, name=cls.DISTINGUISHED_FOLDER_ID, is_distinguished=True)
                )
            except MISSING_FOLDER_ERRORS:
                raise ErrorFolderNotFound('Could not find distinguished folder %r' % cls.DISTINGUISHED_FOLDER_ID)

  ### Instance variables

  `var account`
    Expand source code

        @property
        def account(self):
            if self.root is None:
                return None
            return self.root.account

  `var effective_rights`
  `var parent`
    Expand source code

        @property
        def parent(self):
            if not self.parent_folder_id:
                return None
            if self.parent_folder_id.id == self.id:
                # Some folders have a parent that references itself. Avoid circular references here
                return None
            return self.root.get_folder(self.parent_folder_id)

  `var permission_set`
  `var root`
    Expand source code

        @property
        def root(self):
            return self._root

  ### Methods

  `def clean(self, version=None)`
    Expand source code

        def clean(self, version=None):
            from .roots import RootOfHierarchy
            super().clean(version=version)
            if self.root and not isinstance(self.root, RootOfHierarchy):
                raise ValueError("'root' %r must be a RootOfHierarchy instance" % self.root)

  ### Inherited members

  - `BaseFolder`:
    - `ID_ELEMENT_CLS`
    - `add_field`
    - `deregister`
    - `folder_cls_from_container_class`
    - `folder_sync_state`
    - `get_events`
    - `get_streaming_events`
    - `is_distinguished`
    - `item_sync_state`
    - `register`
    - `remove_field`
    - `subscribe_to_pull`
    - `subscribe_to_push`
    - `subscribe_to_streaming`
    - `supported_fields`
    - `sync_hierarchy`
    - `sync_items`
    - `test_access`
    - `tree`
    - `unsubscribe`
    - `validate_field`
`class PullSubscription (folder, **subscription_kwargs)`
  Expand source code

      class PullSubscription(BaseSubscription):
          def __enter__(self):
              self.subscription_id, watermark = self.folder.subscribe_to_pull(**self.subscription_kwargs)
              return self.subscription_id, watermark

  ### Ancestors

  - BaseSubscription
`class PushSubscription (folder, **subscription_kwargs)`
  Expand source code

      class PushSubscription(BaseSubscription):
          def __enter__(self):
              self.subscription_id, watermark = self.folder.subscribe_to_push(**self.subscription_kwargs)
              return self.subscription_id, watermark

          def __exit__(self, *args, **kwargs):
              # Cannot unsubscribe to push subscriptions
              pass

  ### Ancestors

  - BaseSubscription
`class StreamingSubscription (folder, **subscription_kwargs)`
  Expand source code

      class StreamingSubscription(BaseSubscription):
          def __enter__(self):
              self.subscription_id = self.folder.subscribe_to_streaming(**self.subscription_kwargs)
              return self.subscription_id

  ### Ancestors

  - BaseSubscription