summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJannis M. Hoffmann <jannis@fehcom.de>2024-12-08 16:15:37 +0100
committerJannis M. Hoffmann <jannis@fehcom.de>2024-12-08 16:15:37 +0100
commit57423db1e342b48c970b972a6f18e84e7a7b1a22 (patch)
tree778ecf925dfd7e04eb68edf3ebca4491ae128fcb
parentbdb14d5b5fff9c53ea2684a8180f7a9e55dcc8f3 (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.toml1
-rw-r--r--src/jwebmail/__init__.py2
-rw-r--r--src/jwebmail/model/de.jmhoffmann.jwebmail.mail-storage.varlink35
-rw-r--r--src/jwebmail/model/read_mails.py43
-rw-r--r--src/jwebmail/templates/_bot_nav.html56
-rw-r--r--src/jwebmail/templates/_folders.html6
-rw-r--r--src/jwebmail/templates/_main_table.html6
-rw-r--r--src/jwebmail/templates/_top_nav.html38
-rw-r--r--src/jwebmail/webmail.py105
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>&laquo;</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">
+ &laquo;
+ </a>
+ {% endif %}
+
+ {% if last %}
+ <a class="pagination-next" disabled>&raquo;</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">
+ &raquo;
+ </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>&laquo;</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">
+ &laquo;
+ </a>
+ {% endif %}
+
+ {% if last %}
+ <a class="pagination-next" disabled>&raquo;</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">
+ &raquo;
+ </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