Source code for pymap.backend.maildir.layout


from __future__ import annotations

import errno
import os
import os.path
from abc import abstractmethod, ABCMeta
from collections.abc import Iterable, Sequence
from mailbox import Maildir, NoSuchMailboxError
from typing import TypeAlias, TypeVar, Protocol

__all__ = ['MaildirLayout', 'DefaultLayout', 'FilesystemLayout']

_Parts: TypeAlias = Sequence[str]
_MaildirT = TypeVar('_MaildirT', bound=Maildir)


[docs] class MaildirLayout(Protocol[_MaildirT]): """Manages the folder layout of a :class:`~mailbox.Maildir` inbox. See Also: `Directory Structure <https://wiki.dovecot.org/MailboxFormat/Maildir#line-130>`_ """
[docs] @classmethod def get(cls, path: str, layout: str, maildir_type: type[_MaildirT]) -> MaildirLayout[_MaildirT]: """ Args: path: The root path of the inbox. layout: The layout name, e.g. ``'++'`` or ``'fs'``. maildir_type: The :class:`~mailbox.Maildir` sub-class. Raises: ValueError: The layout name was not recognized. """ if layout == '++': return DefaultLayout(path, maildir_type) elif layout == 'fs': return FilesystemLayout(path, maildir_type) else: raise ValueError(layout)
@property @abstractmethod def path(self) -> str: """The root path of the inbox.""" ...
[docs] @abstractmethod def get_path(self, name: str, delimiter: str) -> str: """Return the path of the sub-folder. Args: name: The nested parts of the folder name, not including inbox. delimiter: The nested sub-folder delimiter string. """ ...
[docs] @abstractmethod def list_folders(self, delimiter: str, top: str = 'INBOX') \ -> Sequence[str]: """Return all folders, starting with ``top`` and traversing through the sub-folder heirarchy. Args: delimiter: The nested sub-folder delimiter string. top: The top of the folder heirarchy to list. Raises: FileNotFoundError: The folder did not exist. """ ...
[docs] @abstractmethod def get_folder(self, name: str, delimiter: str) -> _MaildirT: """Return the existing sub-folder. Args: name: The delimited sub-folder name, not including inbox. delimiter: The nested sub-folder delimiter string. Raises: FileNotFoundError: The folder did not exist. """ ...
[docs] @abstractmethod def add_folder(self, name: str, delimiter: str) -> None: """Add a new sub-folder. Args: name: The delimited sub-folder name, not including inbox. delimiter: The nested sub-folder delimiter string. Raises: FileExistsError: The folder already exists. FileNotFoundError: A parent folder did not exist. """ ...
[docs] @abstractmethod def remove_folder(self, name: str, delimiter: str) -> None: """Remove the existing sub-folder. Args: name: The delimited sub-folder name, not including inbox. delimiter: The nested sub-folder delimiter string. Raises: FileNotFoundError: The folder did not exist. OSError: With :attr:`~errno.ENOTEMPTY`, the folder has sub-folders. """ ...
[docs] @abstractmethod def rename_folder(self, source_name: str, dest_name: str, delimiter: str) -> None: """Rename the existing sub-folder to the destination. Args: source_name: The delimited source sub-folder name. dest_name: The delimited destination sub-folder name. delimiter: The nested sub-folder delimiter string. Raises: FileNotFoundError: The source folder did not exist. FileExistsError: The destination folder already exists. """ ...
class _BaseLayout(MaildirLayout[_MaildirT], metaclass=ABCMeta): def __init__(self, path: str, maildir_type: type[_MaildirT]) -> None: super().__init__() self._path = path self._maildir = maildir_type @property def path(self) -> str: return self._path @classmethod def _split(cls, name: str, delimiter: str) -> _Parts: if name == 'INBOX': return [] return name.split(delimiter) @classmethod def _join(cls, parts: _Parts, delimiter: str) -> str: if not parts: return 'INBOX' return delimiter.join(parts) def _can_remove(self, parts: _Parts) -> bool: return True @abstractmethod def _get_path(self, parts: _Parts) -> str: ... @abstractmethod def _list_folders(self, parts: _Parts) -> Iterable[_Parts]: ... @abstractmethod def _rename_folder(self, source_parts: _Parts, dest_parts: _Parts) -> None: ... def get_path(self, name: str, delimiter: str) -> str: parts = self._split(name, delimiter) return self._get_path(parts) def list_folders(self, delimiter: str, top: str = 'INBOX') \ -> Sequence[str]: parts = self._split(top, delimiter) return [self._join(sub_parts, delimiter) for sub_parts in self._list_folders(parts)] def get_folder(self, name: str, delimiter: str) -> _MaildirT: path = self.get_path(name, delimiter) try: return self._maildir(path, create=False) except NoSuchMailboxError as exc: raise FileNotFoundError(path) from exc def add_folder(self, name: str, delimiter: str) -> None: parts = self._split(name, delimiter) for i in range(1, len(parts) - 1): path = self._get_path(parts[0:i]) if not os.path.isdir(path): raise FileNotFoundError(path) path = self._get_path(parts) self._maildir(path, create=True) maildirfolder = os.path.join(path, 'maildirfolder') with open(maildirfolder, 'x'): pass def remove_folder(self, name: str, delimiter: str) -> None: parts = self._split(name, delimiter) path = self._get_path(parts) if not self._can_remove(parts): path = self._get_path(parts) raise OSError(errno.ENOTEMPTY, 'Directory not empty: ' + repr(path)) for root, dirs, files in os.walk(path, topdown=False): for entry in files: os.remove(os.path.join(root, entry)) for entry in dirs: os.rmdir(os.path.join(root, entry)) os.rmdir(path) def rename_folder(self, source_name: str, dest_name: str, delimiter: str) -> None: source_parts = self._split(source_name, delimiter) dest_parts = self._split(dest_name, delimiter) for i in range(1, len(dest_parts) - 1): parts = dest_parts[0:i] path = self._get_path(parts) if not os.path.isdir(path): name = self._join(parts, delimiter) self.add_folder(name, delimiter) self._rename_folder(source_parts, dest_parts)
[docs] class DefaultLayout(_BaseLayout[_MaildirT]): """The default Maildir++ layout, which uses sub-folder names starting with and using ``.`` as a delimiter for nesting , e.g.:: .Trash/ .Important.To-Do/ .Important.Misc/ Args: path: The root path of the inbox. maildir_type: The :class:`~mailbox.Maildir` class override. """ def _get_path(self, parts: _Parts) -> str: return os.path.join(self._path, self._get_subdir(parts)) @classmethod def _get_subdir(cls, parts: _Parts) -> str: if not parts: return '' return '.' + '.'.join(parts) @classmethod def _get_parts(cls, subdir: str) -> _Parts: if not subdir: return [] return subdir[1:].split('.') def _list_folders(self, parts: _Parts) -> Iterable[_Parts]: subdir = self._get_subdir(parts) path = self._get_path(parts) if not os.path.isdir(path): return yield parts for elem in os.listdir(self._path): if elem in ('new', 'cur', 'tmp'): pass elif not subdir or elem.startswith(subdir + '.'): elem_path = os.path.join(self._path, elem) if os.path.isdir(elem_path): yield self._get_parts(elem) def _rename_folder(self, source_parts: _Parts, dest_parts: _Parts) -> None: subdir = self._get_subdir(source_parts) dest_subdir = self._get_subdir(dest_parts) for elem in os.listdir(self._path): if elem == subdir or elem.startswith(subdir + '.'): elem_path = os.path.join(self._path, elem) if os.path.isdir(elem_path): dest_elem = dest_subdir + elem[len(subdir):] dest_elem_path = os.path.join(self._path, dest_elem) os.rename(elem_path, dest_elem_path)
[docs] class FilesystemLayout(_BaseLayout[_MaildirT]): """The ``fs`` layout, which uses nested sub-directories on the filesystem, e.g.:: Trash/ Important/To-Do/ Important/Misc/ Args: path: The root path of the inbox. maildir_type: The :class:`~mailbox.Maildir` class override. """ def _get_path(self, parts: _Parts) -> str: return os.path.join(self._path, *parts) def _can_remove(self, parts: _Parts) -> bool: path = self._get_path(parts) for elem in os.listdir(path): if elem not in ('new', 'cur', 'tmp'): elem_path = os.path.join(path, elem) if os.path.isdir(elem_path): return False return True def _list_folders(self, parts: _Parts) -> Iterable[_Parts]: path = self._get_path(parts) if not os.path.isdir(path): return yield parts for elem in os.listdir(path): if elem not in ('new', 'cur', 'tmp'): for sub_parts in self._list_folders(list(parts) + [elem]): yield sub_parts def _rename_folder(self, source_parts: _Parts, dest_parts: _Parts) -> None: path = self._get_path(source_parts) dest_path = self._get_path(dest_parts) os.rename(path, dest_path)