aboutsummaryrefslogtreecommitdiff
path: root/mu4web
diff options
context:
space:
mode:
authorHugo Hörnquist <hugo@lysator.liu.se>2022-11-30 04:39:50 +0100
committerHugo Hörnquist <hugo@lysator.liu.se>2022-11-30 04:39:50 +0100
commit1b9fb005faa6087f6b39da0bf7b634324081e890 (patch)
treeed2c6c96858f1d7487bb773c305ec3396356894e /mu4web
parentFix crash on invalid username. (diff)
downloadmu4web-1b9fb005faa6087f6b39da0bf7b634324081e890.tar.gz
mu4web-1b9fb005faa6087f6b39da0bf7b634324081e890.tar.xz
Work.
Diffstat (limited to 'mu4web')
-rw-r--r--mu4web/main.py185
-rw-r--r--mu4web/mu.py22
-rw-r--r--mu4web/user/__init__.py26
-rw-r--r--mu4web/user/local.py11
-rw-r--r--mu4web/user/pam.py6
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)