"""Various HTML "components".""" from email.headerregistry import ( Address, BaseHeader, UniqueAddressHeader, ) from .html_render import HTML from typing import cast, Optional from urllib.parse import urlencode from email.message import EmailMessage from typing import Iterable def dl(entries: Iterable[tuple[HTML, HTML]]) -> HTML: """Build a description list.""" items = [] for k, v in entries: items += [('dt', k), ('dd', v)] return ('dl', items) def format_email(addr: Address) -> list[HTML]: """Format an email address suitable for the headers of the message view.""" mail_addr = f'{addr.username}@{addr.domain}' return [addr.display_name, ' <', mailto(mail_addr), '>'] def mailto(addr: str) -> HTML: """Construct a mailto anchor element.""" return ('a', {'href': f'mailto:{addr}'}, addr) def header_format(key: str, value: BaseHeader) -> HTML: """ Format email headers to HTML. https://docs.python.org/3/library/email.headerregistry.html#email.headerregistry.HeaderRegistry """ if key in ['to', 'cc', 'bcc']: assert isinstance(value, UniqueAddressHeader), \ f"Expected UniqueAddressHeader, got {type(value)}" return ('ul', *[('li', *format_email(addr)) for addr in value.addresses]) elif key == 'from': assert isinstance(value, UniqueAddressHeader), \ f"Expected UniqueAddressHeader, got {type(value)}" return format_email(value.addresses[0]) elif key == 'in-reply-to': id = str(value).strip("<>") return ['<', ('a', {'href': '?' + urlencode({'id': id})}, id), '>'] else: return str(value) def attachement_tree(id: str, mail: EmailMessage, idx: int = 0) -> tuple[HTML, int]: """Construct a tree of all attachements for the given mail.""" ct = mail.get_content_type() fn = mail.get_filename() children = [] _idx = idx for child in mail.iter_parts(): tree, idx = attachement_tree(id, cast(EmailMessage, child), idx + 1) children.append(tree) content: HTML if children: content = ('ul', *children) else: content = [] if fn: body = f'{ct} {fn}' else: body = str(ct) download = {} if mail.get_content_type() == 'application/octet-stream': download['download'] = mail.get_filename() or '' return ('li', ('a', {'data-idx': str(_idx), 'href': '/part?' + urlencode({'id': id, 'idx': _idx}), **download, }, body), content), idx def login_page(returnto: Optional[str] = None) -> HTML: """HTML form for the login page.""" 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', {'id': 'password', 'name': 'password', 'placeholder': 'Lösenord', 'type': 'password'}), ('div', ('input', {'id': 'remember', 'name': 'remember', 'type': 'checkbox'}), ('label', {'for': 'remember'}, 'Kom ihåg mig')), ('input', {'type': 'hidden', 'name': 'returnto', 'value': returnto}) if returnto else [], ('input', {'type': 'submit', 'value': 'Logga in'}), ) def user_info(username: str) -> HTML: """ Return user info for top bar. Includes the users name, and a button for logging out. """ return [('span', username), ('form', {'action': '/logout', 'method': 'POST'}, ('input', {'type': 'submit', 'value': 'Logga ut'}))] def login_prompt() -> HTML: """Return link to the login page.""" return ('a', {'href': '/login'}, 'Logga in') def flashed_messages(messages: list[str] | list[tuple[str, str]]) -> HTML: """ Return Flasks flashed messages, formatted as a list. :param messages: Either a list of messages, or a list of (category, message) messages. See `flask.get_flashed_messages` for more information. """ if not messages: return [] if isinstance(messages[0], tuple): messages = cast(list[tuple[str, str]], messages) return ('ul', {'class': 'flashes'}, *[('li', msg) for (_, msg) in messages]) else: return ('ul', {'class': 'flashes'}, *[('li', msg) for msg in messages]) def include_stylesheet(path: str) -> HTML: """Return HTML for including a stylesheet inside the .""" return ('link', {'type': 'text/css', 'rel': 'stylesheet', 'href': path}) def search_field(q: str) -> HTML: """Build large search form for search page.""" return ('form', {'id': 'searchform', 'action': '/search', 'method': 'GET'}, ('label', {'for': 'search'}, 'Sökförfrågan till Mu'), ('input', {'id': 'search', 'type': 'text', 'placeholder': 'Sök...', 'name': 'q', 'value': q}), ('input', {'type': 'Submit', 'value': 'Sök'}))