diff options
author | Jannis M. Hoffmann <jannis@fehcom.de> | 2024-12-08 16:15:37 +0100 |
---|---|---|
committer | Jannis M. Hoffmann <jannis@fehcom.de> | 2024-12-08 16:15:37 +0100 |
commit | 57423db1e342b48c970b972a6f18e84e7a7b1a22 (patch) | |
tree | 778ecf925dfd7e04eb68edf3ebca4491ae128fcb | |
parent | bdb14d5b5fff9c53ea2684a8180f7a9e55dcc8f3 (diff) |
update for mail-storage version 1.1.0
Now a keyset based pagination is used instead of an offset based one.
This removes the dependency flask-paginate.
URL arguments are taken from the request object in the displayheaders
templates instead of passing them in manually.
Not needed arguments for about render_template are removed.
-rw-r--r-- | pyproject.toml | 1 | ||||
-rw-r--r-- | src/jwebmail/__init__.py | 2 | ||||
-rw-r--r-- | src/jwebmail/model/de.jmhoffmann.jwebmail.mail-storage.varlink | 35 | ||||
-rw-r--r-- | src/jwebmail/model/read_mails.py | 43 | ||||
-rw-r--r-- | src/jwebmail/templates/_bot_nav.html | 56 | ||||
-rw-r--r-- | src/jwebmail/templates/_folders.html | 6 | ||||
-rw-r--r-- | src/jwebmail/templates/_main_table.html | 6 | ||||
-rw-r--r-- | src/jwebmail/templates/_top_nav.html | 38 | ||||
-rw-r--r-- | src/jwebmail/webmail.py | 105 |
9 files changed, 212 insertions, 80 deletions
diff --git a/pyproject.toml b/pyproject.toml index a3f3d37..c611154 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ dependencies = [ "Flask-Babel", "Flask-Login", "Flask-WTF", - "flask-paginate", "email-validator", "varlink", ] diff --git a/src/jwebmail/__init__.py b/src/jwebmail/__init__.py index 0a52677..8b98e7a 100644 --- a/src/jwebmail/__init__.py +++ b/src/jwebmail/__init__.py @@ -36,7 +36,7 @@ else: toml_read_file = dict(load=toml_load, text=True) -__version__ = "2.7.0.dev1" +__version__ = "2.8.0.dev0" csrf = CSRFProtect() diff --git a/src/jwebmail/model/de.jmhoffmann.jwebmail.mail-storage.varlink b/src/jwebmail/model/de.jmhoffmann.jwebmail.mail-storage.varlink index 3be999b..850ff19 100644 --- a/src/jwebmail/model/de.jmhoffmann.jwebmail.mail-storage.varlink +++ b/src/jwebmail/model/de.jmhoffmann.jwebmail.mail-storage.varlink @@ -58,23 +58,54 @@ type MIMEPart ( body: MailBody ) +# sorting by subject is deprecated type Sort ( direction: (asc, desc), - parameter: (date, size, sender) + parameter: (date, size, sender, subject) +) + +type Bound ( + param: string, + id: string ) method Init(unix_user: string, mailbox_path: string) -> () + +# deprecated: use ListSearch instead method List(folder: string, start: int, end: int, sort: Sort) -> (mail_heads: []ListMailHeader) + method Stats(folder: string) -> (mail_count: int, unread_count: int, byte_size: int) + method Show(folder: string, mid: string) -> (mail: Mail) -method Raw(folder: string, mid: string, path: ?string) -> (header: MIMEHeader, body: string) # body is base64 encoded + +# body is base64 encoded +method Raw(folder: string, mid: string, path: ?string) -> (header: MIMEHeader, body: string) + +# deprecated: use ListSearch instead method Search(folder: string, pattern: string) -> (found: []ListMailHeader) + method Folders() -> (folders: []string) + method Move(mid: string, from_folder: string, to_folder: string) -> () + method Remove(folder: string, mid: string) -> () + method AddFolder(name: string) -> (status: (created, skiped)) +method ListSearch( + folder: string, + bound: ?Bound, + direction: (after, before), + limit: int, + sort: Sort, + search: ?string +) -> ( + mail_heads: []ListMailHeader, + first: bool, + last: bool +) + error NotInitialized() error InvalidFolder(folder: string) diff --git a/src/jwebmail/model/read_mails.py b/src/jwebmail/model/read_mails.py index 8a19224..b2e1191 100644 --- a/src/jwebmail/model/read_mails.py +++ b/src/jwebmail/model/read_mails.py @@ -27,7 +27,7 @@ class QMailAuthuser: self._client = None self._connection = None - def read_headers_for(self, folder, start, end, sort): + def list_search(self, folder, bound, after, limit, sort, search): sort_val = dict() if sort[0] == "!": sort = sort[1:] @@ -41,17 +41,32 @@ class QMailAuthuser: case _: raise ValueError(f"invalid sort parameter {sort!r}") - req = self._connection.List(folder=folder, start=start, end=end, sort=sort_val) - return [ - { - "message_handle": lmh["mid"], - "byte_size": lmh["byte_size"], - "unread": lmh["unread"], - "date_received": lmh["rec_date"], - "head": self._mail_header(lmh["header"]), - } - for lmh in req["mail_heads"] - ] + if bound is not None: + param, mid = bound.split("_", 1) + bound = {"param": param, "id": mid} + + req = self._connection.ListSearch( + folder=folder, + bound=bound, + direction="after" if after else "before", + limit=limit, + sort=sort_val, + search=search, + ) + return ( + [ + { + "message_handle": lmh["mid"], + "byte_size": lmh["byte_size"], + "unread": lmh["unread"], + "date_received": lmh["rec_date"], + "head": self._mail_header(lmh["header"]), + } + for lmh in req["mail_heads"] + ], + req["first"], + req["last"], + ) def count(self, folder): resp = self._connection.Stats(folder=folder) @@ -75,10 +90,6 @@ class QMailAuthuser: "body": b64decode(resp["body"]), } - def search(self, pattern, folder): - resp = self._connection.Search(folder=folder, pattern=pattern) - return resp - def folders(self): resp = self._connection.Folders() return list(resp["folders"]) + [""] diff --git a/src/jwebmail/templates/_bot_nav.html b/src/jwebmail/templates/_bot_nav.html index 2634151..bd9e05a 100644 --- a/src/jwebmail/templates/_bot_nav.html +++ b/src/jwebmail/templates/_bot_nav.html @@ -1,7 +1,43 @@ <div class="columns"> - <div class="column"> - {{ pgn.links }} + <div class=column> + <nav class="pagination" role="navigation"> + {% if first %} + <a class="pagination-previous" disabled>«</a> + {% else %} + <a class="pagination-previous" + href="{{ url_for( + 'displayheaders', + folder=request.view_args.folder, + page_bound=page_bound_before, + page_after=0, + per_page=request.args.get('per_page'), + sort=request.args.get('sort'), + search=request.args.get('search'), + ) }}" + aria-label="Next"> + « + </a> + {% endif %} + + {% if last %} + <a class="pagination-next" disabled>»</a> + {% else %} + <a class="pagination-next" + href="{{ url_for( + 'displayheaders', + folder=request.view_args.folder, + page_bound=page_bound_after, + page_after=1, + per_page=request.args.get('per_page'), + sort=request.args.get('sort'), + search=request.args.get('search'), + ) }}" + aria-label="Next"> + » + </a> + {% endif %} + </nav> </div> <div class="column"> @@ -9,9 +45,11 @@ {% if mail_folders|length > 1 %} <form action="{{ url_for('move', folder=folder) }}" id="move-mail" class=is-pulled-left method=POST> <input type=hidden name=csrf_token value="{{ csrf_token() }}"> - <input type=hidden name=sort value="{{ request.args['sort'] }}"> - <input type=hidden name=per_page value="{{ request.args['per_page'] }}"> - <input type=hidden name=page value="{{ request.args['page'] }}"> + <input type=hidden name=sort value="{{ request.args.sort }}"> + <input type=hidden name=search value="{{ request.args.search }}"> + <input type=hidden name=per_page value="{{ request.args.per_page }}"> + <input type=hidden name=page_bound value="{{ request.args.page_bound }}"> + <input type=hidden name=after_page value="{{ request.args.after_page }}"> <div class="field has-addons"> <div class=control> <div class=select> @@ -35,9 +73,11 @@ method=POST class="is-pulled-left ml-2"> <input type=hidden name=csrf_token value="{{ csrf_token() }}"> - <input type=hidden name=sort value="{{ request.args['sort'] }}"> - <input type=hidden name=per_page value="{{ request.args['per_page'] }}"> - <input type=hidden name=page value="{{ request.args['page'] }}"> + <input type=hidden name=sort value="{{ request.args.sort }}"> + <input type=hidden name=search value="{{ request.args.search }}"> + <input type=hidden name=per_page value="{{ request.args.per_page }}"> + <input type=hidden name=page_bound value="{{ request.args.page_bound }}"> + <input type=hidden name=after_page value="{{ request.args.after_page }}"> <div class=control> <input id=remove type=submit class=button value="{{ gettext('Remove') }}"> </div> diff --git a/src/jwebmail/templates/_folders.html b/src/jwebmail/templates/_folders.html index fc4c21d..884188b 100644 --- a/src/jwebmail/templates/_folders.html +++ b/src/jwebmail/templates/_folders.html @@ -5,7 +5,7 @@ <div class="navbar-brand"> <span class=navbar-item> - <b>{{ folder or gettext('Home') }}</b> + <b>{{ request.view_args.folder or gettext('Home') }}</b> </span> <a role="button" class="navbar-burger" data-target="navMenu" id=navbar-toggle> <span aria-hidden="true"></span> @@ -16,7 +16,7 @@ <div class="navbar-menu" id="navMenu"> <div class=navbar-start> - {% for f in mail_folders if f is ne folder %} + {% for f in mail_folders if f is ne request.view_args.get('folder', '') %} <a href="{{ url_for('displayheaders', folder=f, per_page=request.args.get('per_page')) }}" class="navbar-item"> {{ f or gettext('Home') }} </a> @@ -34,7 +34,7 @@ <div class="column is-size-7"> <div class="columns is-multiline is-mobile"> <span class="column is-half-mobile has-text-centered"> - {{ pgn.info }} + {{ total_mails }} Mails </span> {% if total_new_mails %} <span class="column is-half-mobile has-text-centered"> diff --git a/src/jwebmail/templates/_main_table.html b/src/jwebmail/templates/_main_table.html index cc22da4..a7801eb 100644 --- a/src/jwebmail/templates/_main_table.html +++ b/src/jwebmail/templates/_main_table.html @@ -3,10 +3,6 @@ {% for msg in msgs %} <tag class="media {{ 'jwm-new-mail' if msg.unread else '' }}" id="{{ msg.message_handle }}"> - <div class="media-left is-hidden-mobile"> - {{ loop.index + (pgn.page - 1) * pgn.per_page }} - </div> - <div class="media-content"> <div class="columns is-gapless is-multiline"> <div class="column is-10"> @@ -27,7 +23,7 @@ </div> <div class="column is-10"> - <a href="{{ url_for('read', msgid=msg.message_handle, folder=folder) }}"> + <a href="{{ url_for('read', msgid=msg.message_handle, folder=request.view_args.folder) }}"> {{ msg.head.subject or '_' }} </a> </div> diff --git a/src/jwebmail/templates/_top_nav.html b/src/jwebmail/templates/_top_nav.html index 960ae86..a9d385b 100644 --- a/src/jwebmail/templates/_top_nav.html +++ b/src/jwebmail/templates/_top_nav.html @@ -28,7 +28,43 @@ </form> <div class=column> - {{ pgn.links }} + <nav class="pagination" role="navigation"> + {% if first %} + <a class="pagination-previous" disabled>«</a> + {% else %} + <a class="pagination-previous" + href="{{ url_for( + 'displayheaders', + folder=request.view_args.folder, + page_bound=page_bound_before, + page_after=0, + per_page=request.args.get('per_page'), + sort=request.args.get('sort'), + search=request.args.get('search'), + ) }}" + aria-label="Next"> + « + </a> + {% endif %} + + {% if last %} + <a class="pagination-next" disabled>»</a> + {% else %} + <a class="pagination-next" + href="{{ url_for( + 'displayheaders', + folder=request.view_args.folder, + page_bound=page_bound_after, + page_after=1, + per_page=request.args.get('per_page'), + sort=request.args.get('sort'), + search=request.args.get('search'), + ) }}" + aria-label="Next"> + » + </a> + {% endif %} + </nav> </div> </div> diff --git a/src/jwebmail/webmail.py b/src/jwebmail/webmail.py index 99cc542..1b14af0 100644 --- a/src/jwebmail/webmail.py +++ b/src/jwebmail/webmail.py @@ -14,7 +14,6 @@ from urllib.parse import urlparse from flask import abort, current_app, flash, redirect, render_template, request, url_for from flask_babel import gettext, lazy_gettext from flask_login import current_user, logout_user -from flask_paginate import Pagination, get_page_parameter, get_per_page_parameter from flask_wtf import FlaskForm from werkzeug.utils import secure_filename from wtforms import ( @@ -80,14 +79,10 @@ def logout(): def about(): - view_model = { - "scriptadmin": current_app.config["JWEBMAIL"]["ADMIN_MAIL"], - "http_host": request.host, - "request_uri": request.full_path, - "remote_addr": request.remote_addr, - "languages": current_app.extensions["babel"].instance.list_translations(), - } - return render_template("about.html", **view_model) + return render_template( + "about.html", + languages=current_app.extensions["babel"].instance.list_translations(), + ) def displayheaders(folder=""): @@ -96,6 +91,9 @@ def displayheaders(folder=""): if folder and folder not in folders: return render_template("error", error="no_folder", links=folders), 404 + page_bound = request.args.get("page_bound") + page_after = bool(request.args.get("page_after", type=int, default=True)) + per_page = request.args.get("per_page", type=int, default=25) sort = request.args.get("sort", "!date") search = request.args.get("search") @@ -105,41 +103,58 @@ def displayheaders(folder=""): count = get_read_mails_logged_in().count(folder) - page = request.args.get(get_page_parameter(), type=int, default=1) - per_page = request.args.get(get_per_page_parameter(), type=int, default=25) - - pgn = Pagination( - page=page, - per_page=per_page, - total=count["total_mails"], - record_name="mails", - css_framework="bulma", - display_msg=gettext( - "displaying <b>{start} - {end}</b> of <b>{total}</b> {record_name}" - ), - inner_window=1, - outer_window=0, + headers, first, last = get_read_mails_logged_in().list_search( + folder=folder, + bound=page_bound, + after=page_after, + limit=per_page, + sort=sort, + search=search, ) - if search: - headers = get_read_mails_logged_in().search(search, folder) + if headers: + match s: + case "date": + page_bound_after = ( + headers[-1]["date_received"] + "_" + headers[-1]["message_handle"] + ) + page_bound_before = ( + headers[0]["date_received"] + "_" + headers[0]["message_handle"] + ) + case "size": + page_bound_after = ( + str(headers[-1]["byte_size"]) + "_" + headers[-1]["message_handle"] + ) + page_bound_before = ( + str(headers[0]["byte_size"]) + "_" + headers[0]["message_handle"] + ) + case "sender": + page_bound_after = ( + headers[-1]["head"]["from"][0]["address"] + + "_" + + headers[-1]["message_handle"] + ) + page_bound_before = ( + headers[0]["head"]["from"][0]["address"] + + "_" + + headers[0]["message_handle"] + ) else: - headers = get_read_mails_logged_in().read_headers_for( - folder=folder, - start=(pgn.page - 1) * pgn.per_page, - end=(pgn.page - 1) * pgn.per_page + pgn.per_page, - sort=sort, - ) - - vals = { - "folder": folder, - "pgn": pgn, - "msgs": headers, - "mail_folders": folders, - "total_size": count["byte_size"], - "total_new_mails": count["unread_mails"], - } - return render_template("displayheaders.html", **vals) + page_bound_after = None + page_bound_before = None + + return render_template( + "displayheaders.html", + first=first, + last=last, + page_bound_after=page_bound_after, + page_bound_before=page_bound_before, + msgs=headers, + mail_folders=folders, + total_mails=count["total_mails"], + total_size=count["byte_size"], + total_new_mails=count["unread_mails"], + ) def readmail(msgid, folder=""): @@ -190,10 +205,14 @@ def _take_common_req_args(mapping): res = dict() if mapping.get("sort"): res["sort"] = mapping["sort"] + if mapping.get("search"): + res["search"] = mapping["search"] if mapping.get("per_page"): res["per_page"] = mapping["per_page"] - if mapping.get("page"): - res["page"] = mapping["page"] + if mapping.get("page_bound"): + res["page_bound"] = mapping["page_bound"] + if mapping.get("after_page"): + res["after_page"] = mapping["after_page"] return res |