From 9d50287dab79fb10a3a9a9d8dae39cbc3b045e52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Thu, 1 Dec 2022 05:10:14 +0100 Subject: Fix scanning of maildirs. --- mu4web/main.py | 107 ++++++++++++++++++++++++++++++++++++++++----------------- setup.cfg | 1 + 2 files changed, 77 insertions(+), 31 deletions(-) diff --git a/mu4web/main.py b/mu4web/main.py index 68bf144..4d39c1a 100644 --- a/mu4web/main.py +++ b/mu4web/main.py @@ -25,6 +25,7 @@ from user.local import LocalUser from user.pam import PamUser import subprocess +from dataclasses import dataclass import flask from flask import ( @@ -37,6 +38,11 @@ from flask import ( get_flashed_messages ) +try: + from natsort import natsorted +except ModuleNotFoundError: + natsorted = sorted + # # A few operations depend on the index of attachements. These index # are all pre-order traversal indexes of the attachement tree, which @@ -327,53 +333,92 @@ def search_page(q: str, by: Optional[mu.Sortfield], body=main_body)) -def find_maildirs(basedir) -> dict[str, list[str]]: +@dataclass +class Leaf: + name: str + +@dataclass +class Node: + name: str + children: list[Union[Leaf, 'Node']] + + +def build_tree(items: list[list[str]]) -> Node: + groups: dict[str, list[list[str]]] = {} + direct: list[Leaf] = [] + for key, *rest in items: + if rest: + groups.setdefault(key, []).append(rest) + else: + direct.append(Leaf(key)) + + node = Node('root', []) + for key, values in groups.items(): + next = build_tree(values) + next.name = key + node.children.append(next) + node.children.extend(direct) + return node + + +def find(basedir, **flags) -> list[bytes]: + cmdline = ['find', basedir] + for key, value in flags.items(): + cmdline += [f'-{key}', value] + cmdline.append('-print0') + + cmd = subprocess.run(cmdline, capture_output=True) + return cmd.stdout.split(b'\0')[:-1] + + +def find_maildirs(basedir) -> Node: """ Find all maildirs located under basedir. A maildir is defined as any directory which contains a `cur` directory. - - TODO Currently all returns are grouped by their first component, which - fails for directories directly in the root. """ - cmd = subprocess.run(['find', basedir, - '-type', 'd', - '-name', 'cur', - '-print0'], - capture_output=True) - groups: dict[str, list[str]] = {} - # Group by first component - for entry in cmd.stdout.split(b'\0'): - dir = os.path.split(entry)[0][len(basedir) + 1:].decode('UTF-8') - if not dir: - continue - parts = dir.split(os.path.sep) - groups.setdefault(parts[0], []).append(os.path.sep.join(parts[1:])) - return groups + basedir = basedir.rstrip('/') + files = find(basedir, type='d', name='cur') + # + 1 removes leading slash + # - 4 removes '/cur' + dirs = [entry[len(basedir)+1:-4].decode('UTF-8').split(os.path.sep) + for entry in files] + + return build_tree(dirs) + + +def serialize_maildir(maildir: Node, path: list[str] = []) -> HTML: + """Build a (recursive) list from a maildir node.""" + entries: list[HTML] = [] + for node in natsorted(maildir.children, key=lambda n: n.name): + if isinstance(node, Leaf): + parts = '/'.join(path + [node.name]) + url = 'search?' + urlencode({'q': f'maildir:"/{parts}"'}) + entry = ('li', ('a', {'href': url}, + node.name or ('i', 'root'))) + else: + entry = ('li', + ('details', + ('summary', node.name), + serialize_maildir(node, path + [node.name]))) + entries.append(entry) + return ('ul', *entries) def index_page(): data = mu.info() - groups = find_maildirs(data['maildir']) - - entries = [] - - for key, values in sorted(groups.items(), key=lambda p: p[0]): - entries.append(('li', - ('details', - ('summary', key), - ('ul', - [('li', - ('a', {'href': 'search?' + urlencode({'q': f'maildir:"/{key}/{v}"'})}, v)) - for v in sorted(values)])))) + maildirs = find_maildirs(data['maildir'] + '/') + + entries = serialize_maildir(maildirs) + rows = [] for key, value in data.items(): rows.append(('tr', ('td', key), ('td', value))) body = [('div', ('table', ('tbody', rows))), - ('div', ('ul', entries)), + ('div', entries), ] return render_document(page_base(title='Mail index', diff --git a/setup.cfg b/setup.cfg index bf73722..aba19bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,6 +15,7 @@ install_requires = flask >= 2.2.2 flask-login >= 0.6 urllib3 >= 1.26 +# optionally natsort >= 8.2 setup_requires = setuptools py_modules = mu4web -- cgit v1.2.3