"""Wrapper for the `mu` command line.""" import email.message import email.policy from email.parser import BytesParser import subprocess from subprocess import PIPE import xml.dom.minidom import xml.dom from xdg.BaseDirectory import xdg_cache_home import os.path from . import xapian from datetime import datetime from os import PathLike from pathlib import Path from typing import ( Optional, TypedDict, ) parser = BytesParser(policy=email.policy.default) def find_file(id: str) -> Optional[PathLike[str]]: """ Find the file system location for mail with given id. :param id: A normalized message id. :returns: Either the file system path for a message with that id, or None if no matches were found. :raises MuError: On all other failure modes. """ cmd = subprocess.run(['mu', 'find', '-u', f'i:{id}', '--fields', 'l'], stdout=PIPE) filename = cmd.stdout.decode('UTF-8').strip() if cmd.returncode == 4: return None if cmd.returncode != 0: raise MuError(cmd.returncode) return Path(filename) def get_mail(id: str) -> email.message.Message: """ Lookup email by Message-ID. :raises MuError: """ filename = find_file(id) if not filename: # TODO better error here raise MuError(1) with open(filename, "rb") as f: mail = parser.parse(f) return mail class MuError(Exception): """One of the errors which mu can return.""" codes = { 1: 'General Error', 2: 'No Matches', 4: 'Database is corrupted' } def __init__(self, returncode: int): self.returncode: int = returncode self.msg: str = MuError.codes.get(returncode, 'Unknown Error') def __repr__(self) -> str: return f'MuError({self.returncode}, "{self.msg}")' def __str__(self) -> str: return repr(self) def search(query: str, sortfield: Optional[str] = 'subject', reverse: bool = False) -> list[dict[str, str]]: """ Search the mu database for messages. :param query: Search query as per mu-find(1). :param sortfield: Field to sort the values by :param reverse: If the sort should be reversed :returns: >>> {'from': 'Hugo Hörnquist ', 'date': '1585678375', 'size': '377', 'msgid': 'SAMPLE-ID@localhost', 'path': '/home/hugo/.local/var/mail/INBOX/cur/filename', 'maildir': '/INBOX' } """ if not query: raise ValueError('Query required for mu_search') cmdline = ['mu', 'find', '--format=xml', query] if sortfield: cmdline.extend(['--sortfield', sortfield]) if reverse: cmdline.append('--reverse') cmd = subprocess.run(cmdline, capture_output=True) if cmd.returncode == 1: raise MuError(cmd.returncode) if cmd.returncode == 4: # no matches return [] if cmd.returncode != 0: raise MuError(cmd.returncode) dom = xml.dom.minidom.parseString(cmd.stdout.decode('UTF-8')) message_list = [] messages = dom.childNodes[0] assert messages.localName == 'messages' for message in messages.childNodes: msg_dict = {} if message.nodeType != xml.dom.Node.ELEMENT_NODE: continue for kv in message.childNodes: if kv.nodeType != xml.dom.Node.ELEMENT_NODE: continue msg_dict[kv.localName] = kv.childNodes[0].data message_list.append(msg_dict) return message_list def base_directory() -> PathLike[str]: """ Return where where mu stores its files. Defaults to $XDG_CACHE_HOME/mu, but can be changed through the environment variable MUHOME. TODO make this configurable. """ return Path(os.getenv('MUHOME') or os.path.join(xdg_cache_home, 'mu')) class MuInfo(TypedDict): """Metadata about the mu database.""" database_path: str changed: datetime created: datetime indexed: datetime maildir: str schema_version: str messages_in_store: int def info() -> MuInfo: """Collect metadata about the mu database.""" db = os.path.join(base_directory(), "xapian") def f(key: str) -> datetime: return datetime.fromtimestamp(int(xapian.metadata_get(db, key), 16)) changed = f('changed') created = f('created') indexed = f('indexed') maildir = xapian.metadata_get(db, 'maildir') schema_version = xapian.metadata_get(db, 'schema-version') cmd = subprocess.Popen(['xapian-delve', '-V3', '-1', db], stdout=subprocess.PIPE) # Start at minus one to ignore header count = -1 if cmd.stdout: for line in cmd.stdout: count += 1 return { 'database_path': db, 'messages_in_store': count, 'changed': changed, 'created': created, 'indexed': indexed, 'maildir': maildir, 'schema_version': schema_version, }