aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHugo Hörnquist <hugo@lysator.liu.se>2022-11-28 19:34:35 +0100
committerHugo Hörnquist <hugo@lysator.liu.se>2022-11-28 19:34:35 +0100
commitd8a7a022df815d8254c4a9674ef1143f50caa277 (patch)
treedc0e6d2dea1b821bca9c1c15bd7a6413b80b8791
parentReplace manual cookies with flask session. (diff)
downloadmu4web-d8a7a022df815d8254c4a9674ef1143f50caa277.tar.gz
mu4web-d8a7a022df815d8254c4a9674ef1143f50caa277.tar.xz
Documentation.
-rw-r--r--html_render.py44
-rw-r--r--main.py2
-rw-r--r--mu.py58
-rwxr-xr-xpassword.py23
4 files changed, 113 insertions, 14 deletions
diff --git a/html_render.py b/html_render.py
index 533ae7c..99140ad 100644
--- a/html_render.py
+++ b/html_render.py
@@ -1,23 +1,24 @@
import html
from typing import (
+ Callable,
TypeAlias,
Union,
- Callable,
- Optional,
- cast,
)
-HTML: TypeAlias = Union[tuple, list, Callable[[], str], None,
- str, int, float]
+HTML: TypeAlias = Union[tuple,
+ list['HTML'],
+ Callable[[], str],
+ None, str, int, float]
standalones = ['hr', 'br', 'meta']
+"""Tags which can't have a closing tag."""
def _render_document(document: HTML) -> str:
- if type(document) == tuple:
+ if isinstance(document, tuple):
tag, *body = document
- if body and type(body[0]) == dict:
+ if body and isinstance(body[0], dict):
print(body[0])
attributes = ' '.join(f'{a}="{html.escape(b)}"'
for a, b in body[0].items())
@@ -25,14 +26,18 @@ def _render_document(document: HTML) -> str:
start = f'<{tag} {attributes}>'
else:
start = f'<{tag}>'
+
if tag in standalones:
return start
else:
- items = ''.join(_render_document(b) for b in body)
+ if body:
+ items = ''.join(_render_document(b) for b in body)
+ else:
+ items = ''
return start + f'{items}</{tag}>'
elif callable(document):
return str(document())
- elif type(document) == list:
+ elif isinstance(document, list):
return ''.join(_render_document(e) for e in document)
elif document is None:
return ''
@@ -42,4 +47,25 @@ def _render_document(document: HTML) -> str:
def render_document(document: HTML) -> str:
+ """
+ Render an HTML structure to an Html string.
+
+ The following Python types are converted as follows:
+ - Tuples
+ - The first value becomes the tags name
+ - The second value, if a dictionary, becomes the tags attributes
+ - All following values (including the second if not a dictionary)
+ gets individually passed to render_document.
+ - Lists
+ Each element gets passed to render_document
+ - Callable[[], str]
+ Gets called, and its output is included verbatim. Useful for
+ including strings which shouldn't be escaped.
+ - str
+ Gets escaped, and included
+ - int, float
+ Gets included as their default string representation.
+ - None
+ Becomes an empty string
+ """
return '<!doctype html>\n' + _render_document(document)
diff --git a/main.py b/main.py
index f1c7b27..8315d3b 100644
--- a/main.py
+++ b/main.py
@@ -1,10 +1,8 @@
from email.message import EmailMessage
from email.headerregistry import Address
from urllib.parse import urlencode
-import http.cookies
import password
from password import Passwords
-from uuid import uuid4
import os
from typing import (
Optional,
diff --git a/mu.py b/mu.py
index 93816f4..9337747 100644
--- a/mu.py
+++ b/mu.py
@@ -1,3 +1,7 @@
+"""
+Wrapper for the `mu` command line.
+"""
+
import email.message
import email.policy
from email.parser import BytesParser
@@ -6,16 +10,29 @@ from subprocess import PIPE
import xml.dom.minidom
import xml.dom
+from typing import (
+ Literal,
+ Union,
+)
parser = BytesParser(policy=email.policy.default)
def get_mail(id: str) -> email.message.Message:
+ """
+ Lookup email by Message-ID.
+
+ [Raises]
+ MuError
+ """
cmd = subprocess.run(['mu', 'find', '-u', f'i:{id}',
'--fields', 'l'],
stdout=PIPE)
filename = cmd.stdout.decode('UTF-8').strip()
+ if cmd.returncode != 0:
+ raise MuError(cmd.returncode)
+
with open(filename, "rb") as f:
mail = parser.parse(f)
return mail
@@ -36,7 +53,46 @@ class MuError(Exception):
return f'MuError({self.returncode}, "{self.msg}")'
-def mu_search(query, sortfield='subject', reverse=False):
+Sortfield = Union[Literal['cc'],
+ Literal['c'],
+ Literal['bcc'],
+ Literal['h'],
+ Literal['date'],
+ Literal['d'],
+ Literal['from'],
+ Literal['f'],
+ Literal['maildir'],
+ Literal['m'],
+ Literal['msgid'],
+ Literal['i'],
+ Literal['prio'],
+ Literal['p'],
+ Literal['subject'],
+ Literal['s'],
+ Literal['to'],
+ Literal['t'],
+ Literal['list'],
+ Literal['v']]
+
+
+
+def mu_search(query, sortfield: Sortfield = 'subject', reverse: bool = False) -> list[dict[str, str]]:
+ """
+ [Parameters]
+ query - Search query as per mu-find(1).
+ sortfield - Field to sort the values by
+ reverse - If the sort should be reversed
+
+ [Returns]
+ >>> {'from': 'Hugo Hörnquist <hugo@example.com>',
+ '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',
diff --git a/password.py b/password.py
index 16192be..ff0df21 100755
--- a/password.py
+++ b/password.py
@@ -1,4 +1,11 @@
#!/usr/bin/env python3
+
+"""
+Simple password store, backed by a JSON file.
+
+Also contains an entry point for managing the store.
+"""
+
import hashlib
import json
import os
@@ -28,6 +35,12 @@ class PasswordEntry(TypedDict):
class Passwords:
+ """
+ Simple password store.
+
+ [Parameters]
+ fname - Path of json file to load and store from.
+ """
def __init__(self, fname: os.PathLike):
self.fname = fname
self.db: dict[str, PasswordEntry]
@@ -38,6 +51,7 @@ class Passwords:
self.db = {}
def save(self) -> None:
+ """Dump current data to disk."""
try:
with open(os.fspath(self.fname) + '.tmp', 'w') as f:
json.dump(self.db, f)
@@ -47,6 +61,7 @@ class Passwords:
print(f'Saving password failed {e}')
def add(self, username: str, password: str) -> None:
+ """Add (or modify) entry in store."""
if cur := self.db.get(username):
salt = cur['salt']
hashed = hashlib.sha256((salt + password).encode('UTF-8'))
@@ -65,6 +80,7 @@ class Passwords:
}
def validate(self, username: str, password: str) -> bool:
+ """Check if user exists, and if it has a correct password."""
# These shall fail when key is missing
data = self.db[username]
proc = hash_methods[data['method']]
@@ -72,8 +88,7 @@ class Passwords:
return data['hash'] == digest
-if __name__ == '__main__':
-
+def main():
import argparse
parser = argparse.ArgumentParser()
@@ -99,3 +114,7 @@ if __name__ == '__main__':
print(passwords.validate(args.username, args.password))
else:
parser.print_help()
+
+
+if __name__ == '__main__':
+ main()