diff options
author | Hugo Hörnquist <hugo@lysator.liu.se> | 2022-11-30 04:39:50 +0100 |
---|---|---|
committer | Hugo Hörnquist <hugo@lysator.liu.se> | 2022-11-30 04:39:50 +0100 |
commit | 1b9fb005faa6087f6b39da0bf7b634324081e890 (patch) | |
tree | ed2c6c96858f1d7487bb773c305ec3396356894e /mu4web | |
parent | Fix crash on invalid username. (diff) | |
download | mu4web-1b9fb005faa6087f6b39da0bf7b634324081e890.tar.gz mu4web-1b9fb005faa6087f6b39da0bf7b634324081e890.tar.xz |
Work.
Diffstat (limited to 'mu4web')
-rw-r--r-- | mu4web/main.py | 185 | ||||
-rw-r--r-- | mu4web/mu.py | 22 | ||||
-rw-r--r-- | mu4web/user/__init__.py | 26 | ||||
-rw-r--r-- | mu4web/user/local.py | 11 | ||||
-rw-r--r-- | mu4web/user/pam.py | 6 |
5 files changed, 194 insertions, 56 deletions
diff --git a/mu4web/main.py b/mu4web/main.py index 7c3d7bd..05a8408 100644 --- a/mu4web/main.py +++ b/mu4web/main.py @@ -16,8 +16,11 @@ from typing import ( Optional, cast, ) -from mu import mu_search, get_mail +from mu import get_mail +import mu from html_render import HTML, render_document +from user.local import LocalUser +from user.pam import PamUser from flask import ( @@ -57,14 +60,37 @@ def header_format(key: str, value) -> HTML: style: HTML = lambda: """ + body, html { + padding: 0; + margin: 0; + } nav { display: block; - width: 100%; height: 4em; color: white; background-color: darkgrey; } + nav, footer { + width: 100%; + padding-left: 1em; + padding-right: 1em; + box-sizing: border-box; + } + + menu { + margin: 0; + padding: 0; + display: flex; + justify-content: space-between; + height: 100%; + } + + menu > li { + display: flex; + align-items: center; + } + dl { display: grid; grid-template-columns: 10ch auto; @@ -84,6 +110,49 @@ style: HTML = lambda: """ background-color: lightblue; } + .loginform { + display: grid; + grid-template-columns: auto 1fr; + width: 50ch; + } + + .loginform label { + padding: 0.5ex; + } + + .loginform input[type="submit"] { + grid-column: 1/3; + } + + main { + margin: 1em; + } + + footer menu { + justify-content: space-around; + } + + footer a { + color: darkgrey; + } + + footer { + border-top: 1px solid grey; + } + + #searchform { + display: grid; + grid-template-columns: 1fr auto; + } + + #searchform input { + grid-column: 1/3; + } + + #searchform input[type="submit"] { + grid-column: 2; + } + """ @@ -111,16 +180,18 @@ def attachement_tree(mail: EmailMessage) -> HTML: def login_page(returnto: Optional[str] = None) -> HTML: - return ('form', {'action': '/login', 'method': 'POST'}, - ('input', {'name': 'username', 'placeholder': 'Username'}), + return ('form', {'action': '/login', 'method': 'POST', 'class': 'loginform'}, + ('label', {'for': 'username'}, 'Användarnamn'), + ('input', {'id': 'username', 'name': 'username', 'placeholder': 'Användarnamn'}), + ('label', {'for': 'password'}, 'Lösenord'), ('input', {'type': 'password', - 'placeholder': 'Password', + 'placeholder': 'Lösenord', 'name': 'password'}), ('input', {'type': 'hidden', 'name': 'returnto', 'value': returnto}) if returnto else [], - ('input', {'type': 'submit'}), + ('input', {'type': 'submit', 'value': 'Logga in'}), ) @@ -149,10 +220,21 @@ def page_base(title: Optional[str] = None, ), ('body', ('nav', - user_info(current_user.get_id()) if current_user.is_authenticated else login_prompt() - ), - flashed_messages(), - body)) + ('menu', + ('li', + ('h1', 'Mu4Web'), + ('li', + user_info(current_user.get_id()) + if current_user.is_authenticated else login_prompt()) + ))), + ('main', + flashed_messages(), + body), + ('footer', + ('menu', + ('li', ('a', {'href': 'https://www.djcbsoftware.nl/code/mu/'}, 'mu')), + ('li', ('a', {'href': 'https://git.hornquist.se/mu4web'}, 'Source')), + )))) def response_for(id: str, username: Optional[str] = None) -> str: @@ -198,12 +280,17 @@ def response_for(id: str, username: Optional[str] = None) -> str: def search_field(q: str) -> HTML: - return ('form', {'action': '/search', 'method': 'GET'}, + return ('form', {'id': 'searchform', + 'action': '/search', + 'method': 'GET'}, ('label', {'for': 'search'}, 'Mu Search Query'), - ('textarea', {'id': 'search', 'name': 'q'}, - q), - ('input', {'type': 'Submit'})) + ('input', {'id': 'search', + 'type': 'text', + 'placeholder': 'Sök...', + 'name': 'q', + 'value': q}), + ('input', {'type': 'Submit', 'value': 'Sök'})) def search_result(q, by, reverse) -> HTML: @@ -211,7 +298,7 @@ def search_result(q, by, reverse) -> HTML: # keys = ['from', 'to', 'subject', 'date', 'size', 'maildir', 'msgid'] keys = ['from', 'to', 'subject', 'date'] - rows = mu_search(q, by, reverse) + rows = mu.search(q, by, reverse) body: list[tuple] = [] for row in rows: rowdata = [] @@ -224,15 +311,16 @@ def search_result(q, by, reverse) -> HTML: body.append(('tr', rowdata)) - # TODO print number of results - - return ('table', - ('thead', - ('tr', - [('th', m.title()) for m in keys])), - ('tbody', - body - )) + if len(rows) == 0: + return "No results" + else: + return ('div', + ('p', f"{len(rows)} träffar"), + ('table', + ('thead', + ('tr', + [('th', m.title()) for m in keys])), + ('tbody', body))) def search_page(q, by): @@ -245,13 +333,26 @@ def search_page(q, by): body=main_body)) +def mu_info(): + d = mu.info() + rows = [] + for key, value in d.items(): + rows.append(('tr', + ('td', key), + ('td', value))) + return ('table', ('tbody', rows)) + + + def index_page(): ids = [ 'CAEzixGsw-4zJ8_ejK_vDgmcQ9s-MbBc-ho+HL4arV4a+ghOOPg@mail.gmail.com', 'CA+pcBt-gLb0GtbFOjJ5_7Q_WXtqApVPQ9w-3O7GH=VqCEQat6g@mail.gmail.com', ] - body = [('h1', "Sample ID's"), + body = [ mu_info(), + ('hr',), + ('h1', "Sample ID's"), ('ul', [('li', ('a', {'href': '?' + urlencode({'id': id})}, id)) for id in ids] @@ -262,42 +363,15 @@ def index_page(): body=body)) -passwords: Passwords = password.Passwords(cast(os.PathLike, 'passwords.json')) - - app = Flask(__name__) login_manager.init_app(app) app.secret_key = 'THIS IS A RANDOM STRING' -class User: - def __init__(self, username: str): - self._username = username - self._authenticated = False - - # @property - def is_authenticated(self): - return self._authenticated - - # @property - def is_active(self): - return True - - # @property - def is_anonymous(self): - return False - - # @property - def get_id(self): - return self._username - - - - @login_manager.user_loader def load_user(user_id): # return User.get(user_id) - return User(user_id) + return LocalUser(user_id) @app.route('/') @@ -319,6 +393,7 @@ def search_page_(): request.args.get('by', None)) +# TODO this page is really weird if you are already logged in @app.route('/login', methods=['GET']) def login_page_(): body = login_page(request.args.get('returnto')) @@ -331,8 +406,8 @@ def login_form(): username = request.form['username'] password = request.form['password'] - user = User(username) - if passwords.validate(username, password): + user = PamUser(username) + if user.validate(password): login_user(user) else: flash('Invalid username or password') diff --git a/mu4web/mu.py b/mu4web/mu.py index d52a362..c8b278e 100644 --- a/mu4web/mu.py +++ b/mu4web/mu.py @@ -80,7 +80,7 @@ Sortfield = Union[Literal['cc'], -def mu_search(query, sortfield: Optional[Sortfield] = 'subject', reverse: bool = False) -> list[dict[str, str]]: +def search(query, sortfield: Optional[Sortfield] = 'subject', reverse: bool = False) -> list[dict[str, str]]: """ [Parameters] query - Search query as per mu-find(1). @@ -108,6 +108,11 @@ def mu_search(query, sortfield: Optional[Sortfield] = 'subject', reverse: bool = cmdline.append('--reverse') print(cmdline) 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')) @@ -126,3 +131,18 @@ def mu_search(query, sortfield: Optional[Sortfield] = 'subject', reverse: bool = message_list.append(msg_dict) return message_list + + +def info(): + cmd = subprocess.Popen('mu info --nocolor'.split(' '), + stdout=subprocess.PIPE, + text=True) + out = {} + for line in cmd.stdout: + if not line: + continue + if line[0] == '+': + continue + key, *value = [s.strip() for s in line.split('|') if s and not s.isspace()] + out[key] = '|'.join(value) + return out diff --git a/mu4web/user/__init__.py b/mu4web/user/__init__.py new file mode 100644 index 0000000..490bcee --- /dev/null +++ b/mu4web/user/__init__.py @@ -0,0 +1,26 @@ +import os + + +class User: + def __init__(self, username: str): + self._username = username + self._authenticated = False + + # @property + def is_authenticated(self): + return self._authenticated + + # @property + def is_active(self): + return True + + # @property + def is_anonymous(self): + return False + + # @property + def get_id(self): + return self._username + + def validate(self, password: str) -> bool: + ... diff --git a/mu4web/user/local.py b/mu4web/user/local.py new file mode 100644 index 0000000..c4485ce --- /dev/null +++ b/mu4web/user/local.py @@ -0,0 +1,11 @@ +import password +from password import Passwords +from typing import cast +import os +from . import User + +passwords: Passwords = password.Passwords(cast(os.PathLike, 'passwords.json')) + +class LocalUser(User): + def validate(self, password: str) -> bool: + return passwords.validate(self._username, password) diff --git a/mu4web/user/pam.py b/mu4web/user/pam.py new file mode 100644 index 0000000..7942f3b --- /dev/null +++ b/mu4web/user/pam.py @@ -0,0 +1,6 @@ +from . import User +import pam + +class PamUser(User): + def validate(self, password): + return pam.authenticate(self._username, password) |