diff options
Diffstat (limited to 'src/jwebmail')
-rw-r--r-- | src/jwebmail/__init__.py | 53 | ||||
-rw-r--r-- | src/jwebmail/model/read_mails.py | 13 | ||||
-rw-r--r-- | src/jwebmail/read_mails.py | 65 | ||||
-rw-r--r-- | src/jwebmail/render_mail.py | 4 | ||||
-rw-r--r-- | src/jwebmail/templates/_main_table.html | 4 | ||||
-rw-r--r-- | src/jwebmail/templates/about.html | 6 | ||||
-rw-r--r-- | src/jwebmail/templates/login.html | 8 | ||||
-rw-r--r-- | src/jwebmail/translations/de/LC_MESSAGES/messages.po | 106 | ||||
-rw-r--r-- | src/jwebmail/webmail.py | 139 |
9 files changed, 184 insertions, 214 deletions
diff --git a/src/jwebmail/__init__.py b/src/jwebmail/__init__.py index db0c796..b5279fb 100644 --- a/src/jwebmail/__init__.py +++ b/src/jwebmail/__init__.py @@ -1,8 +1,8 @@ -import os.path as ospath +import os.path as os_path import pwd import sys -from flask import Flask +from flask import Flask, g from flask_babel import Babel, get_locale from flask_login import LoginManager, login_required from jinja2 import ChainableUndefined @@ -21,6 +21,7 @@ from .webmail import ( readmail, sendmail, writemail, + DEFAULT_LANGUAGE, ) if sys.version_info >= (3, 11): @@ -37,9 +38,9 @@ def validate_config(app): assert "@" in conf["JWEBMAIL"]["ADMIN_MAIL"] assert pwd.getpwnam(conf["JWEBMAIL"]["READ_MAILS"]["MAILBOX_USER"]) - assert ospath.isdir(conf["JWEBMAIL"]["READ_MAILS"]["MAILBOX"]) - assert ospath.isfile(conf["JWEBMAIL"]["READ_MAILS"]["AUTHENTICATOR"]) - assert ospath.isfile(conf["JWEBMAIL"]["READ_MAILS"]["BACKEND"]) + assert os_path.isdir(conf["JWEBMAIL"]["READ_MAILS"]["MAILBOX"]) + assert os_path.isfile(conf["JWEBMAIL"]["READ_MAILS"]["AUTHENTICATOR"]) + assert os_path.isfile(conf["JWEBMAIL"]["READ_MAILS"]["BACKEND"]) def create_app(): @@ -49,7 +50,7 @@ def create_app(): app.config.from_file("../../jwebmail.toml", load=toml_load, text=False) validate_config(app) - Babel(app, locale_selector=lambda: "de") + Babel(app, locale_selector=lambda: g.get("lang_code", DEFAULT_LANGUAGE)) app.cli.add_command(compile_css_command) @@ -58,32 +59,62 @@ def create_app(): login_manager.user_loader(load_user) login_manager.init_app(app) + add_view_funcs(app) + route(app) + @app.context_processor def inject_version(): return {"version": "4.0", "get_locale": get_locale, "format_mail": format_mail} - add_view_funcs(app) - route(app) + @app.url_defaults + def add_language_code(endpoint, values): + if "lang_code" in values: + return + if app.url_map.is_endpoint_expecting(endpoint, "lang_code"): + values["lang_code"] = g.get("lang_code", DEFAULT_LANGUAGE) + + @app.url_value_preprocessor + def pull_lang_code(endpoint, values): + g.lang_code = ( + values.pop("lang_code", DEFAULT_LANGUAGE) if values else DEFAULT_LANGUAGE + ) return app def route(app): app.add_url_rule("/", view_func=login, methods=["GET", "POST"]) + app.add_url_rule("/<lang_code>/", view_func=login, methods=["GET", "POST"]) app.add_url_rule("/about", view_func=about) + app.add_url_rule("/<lang_code>/about", view_func=about) + app.add_url_rule("/logout", view_func=logout) + app.add_url_rule("/<lang_code>/logout", view_func=logout) dh = login_required(displayheaders) app.add_url_rule("/home/", view_func=dh) app.add_url_rule("/home/<folder>", view_func=dh) + app.add_url_rule("/<lang_code>/home/", view_func=dh) + app.add_url_rule("/<lang_code>/home/<folder>", view_func=dh) + lr_readmail = login_required(readmail) + app.add_url_rule("/read/<msgid>", endpoint="read", view_func=lr_readmail) + app.add_url_rule("/read/<folder>/<msgid>", endpoint="read", view_func=lr_readmail) app.add_url_rule( - "/read/<msgid>", endpoint="read", view_func=login_required(readmail) + "/<lang_code>/read/<msgid>", endpoint="read", view_func=lr_readmail ) - app.add_url_rule("/raw/<msgid>", endpoint="raw", view_func=login_required(rawmail)) + app.add_url_rule( + "/<lang_code>/read/<folder>/<msgid>", endpoint="read", view_func=lr_readmail + ) + + lr_rawmail = login_required(rawmail) + app.add_url_rule("/raw/<msgid>", endpoint="raw", view_func=rawmail) + app.add_url_rule("/raw/<folder>/<msgid>", endpoint="raw", view_func=rawmail) - app.add_url_rule("/write", endpoint="write", view_func=login_required(writemail)) + lr_writemail = login_required(writemail) + app.add_url_rule("/write", endpoint="write", view_func=lr_writemail) + app.add_url_rule("/<lang_code>/write", endpoint="write", view_func=lr_writemail) app.add_url_rule( "/write", endpoint="send", view_func=login_required(sendmail), methods=["POST"] ) diff --git a/src/jwebmail/model/read_mails.py b/src/jwebmail/model/read_mails.py index f82b601..291fa1e 100644 --- a/src/jwebmail/model/read_mails.py +++ b/src/jwebmail/model/read_mails.py @@ -31,13 +31,12 @@ class QMailAuthuser: shell=True, timeout=2, ) - match completed_proc.returncode: - case 0: - return True - case 1: - return False - case n: - raise QMAuthError("authentication error", n) + if completed_proc.returncode == 0: + return True + if completed_proc.returncode == 1: + return False + else: + raise QMAuthError("authentication error", completed_proc.returncode) except TimeoutExpired: return False diff --git a/src/jwebmail/read_mails.py b/src/jwebmail/read_mails.py index e1c3b8c..915567c 100644 --- a/src/jwebmail/read_mails.py +++ b/src/jwebmail/read_mails.py @@ -1,11 +1,17 @@ -import dbm -import shelve - +import redis from flask import current_app, g -from flask_login import current_user +from flask_login import UserMixin, current_user from .model.read_mails import QMailAuthuser +EXPIRATION_SEC = 60 * 60 * 25 + + +class JWebmailUser(UserMixin): + def __init__(self, mail_addr, password): + self.id = mail_addr + self.password = password + def build_qma(username, password): authenticator = current_app.config["JWEBMAIL"]["READ_MAILS"]["AUTHENTICATOR"] @@ -22,29 +28,52 @@ def login(username, password): return build_qma(username, password).verify_user() -def add_user(user): - with shelve.open("user_sessions", flag="c") as s: - s[user.get_id()] = user +def add_user(user: JWebmailUser): + passwd = current_app.config["JWEBMAIL"]["READ_MAILS"]["SESSION_STORE_PASSWD"] + r = redis.Redis( + host="localhost", + port=6379, + decode_responses=True, + protocol=3, + username="jwebmail", + password=passwd, + ) + r.setex(f"jwm:user:{user.get_id()}", EXPIRATION_SEC, user.password) -def load_user(username): - try: - with shelve.open("user_sessions", flag="r") as s: - user = s[username] - return user - except dbm.error: - return None - except KeyError: +def load_user(username: str) -> JWebmailUser: + passwd = current_app.config["JWEBMAIL"]["READ_MAILS"]["SESSION_STORE_PASSWD"] + r = redis.Redis( + host="localhost", + port=6379, + decode_responses=True, + protocol=3, + username="jwebmail", + password=passwd, + ) + passwd = r.getex(f"jwm:user:{username}", EXPIRATION_SEC) + if passwd is None: return None + return JWebmailUser(username, passwd) def get_read_mails_logged_in(): if "read_mails" in g: return g.read_mails - with shelve.open("user_sessions", flag="r") as s: - user_data = s[current_user.get_id()] + passwd = current_app.config["JWEBMAIL"]["READ_MAILS"]["SESSION_STORE_PASSWD"] + r = redis.Redis( + host="localhost", + port=6379, + decode_responses=True, + protocol=3, + username="jwebmail", + password=passwd, + ) + passwd = r.get(f"jwm:user:{current_user.get_id()}") + if passwd is None: + raise KeyError(current_user.get_id()) - qma = build_qma(current_user.get_id(), user_data.password) + qma = build_qma(current_user.get_id(), passwd) g.read_mails = qma return qma diff --git a/src/jwebmail/render_mail.py b/src/jwebmail/render_mail.py index 0e5406c..804b200 100644 --- a/src/jwebmail/render_mail.py +++ b/src/jwebmail/render_mail.py @@ -143,7 +143,9 @@ def mime_render(maintype, subtype, content, path): renderer = MIMERenderSubs.get((maintype, subtype)) or MIMERenderSubs.get(maintype) if not renderer: - return f'<p class="jwm-body-unsupported">Unsupported MIME type of <code>{maintype}/{subtype}</code>.</p>\n' + typ = f"<code>{maintype}/{subtype}</code>" + msg = gettext("Unsupported MIME type of {mime_type}.").format(mime_type=typ) + return f'<p class="jwm-body-unsupported">{msg}</p>\n' return renderer(subtype, content, path) diff --git a/src/jwebmail/templates/_main_table.html b/src/jwebmail/templates/_main_table.html index 02f9f81..b331087 100644 --- a/src/jwebmail/templates/_main_table.html +++ b/src/jwebmail/templates/_main_table.html @@ -18,7 +18,9 @@ </div> <div class="column is-10"> - <a href="{{ url_for('read', msgid=msg.message_handle) }}">{{ msg.head.subject or '_' }}</a> + <a href="{{ url_for('read', msgid=msg.message_handle, folder=folder) }}"> + {{ msg.head.subject or '_' }} + </a> </div> <div class="column is-2"> diff --git a/src/jwebmail/templates/about.html b/src/jwebmail/templates/about.html index e197275..836b668 100644 --- a/src/jwebmail/templates/about.html +++ b/src/jwebmail/templates/about.html @@ -40,7 +40,11 @@ <ul> {% for lang in languages %} <li> - {{ get_locale().languages[lang.language] }} + {% if lang == get_locale() %} + <a href="{{ url_for('about', lang_code=lang.language) }}"><strong>{{ get_locale().languages[lang.language] }}</strong></a> + {% else %} + <a href="{{ url_for('about', lang_code=lang.language) }}">{{ get_locale().languages[lang.language] }}</a> + {% endif %} </li> {% endfor %} </ul> diff --git a/src/jwebmail/templates/login.html b/src/jwebmail/templates/login.html index 2915de3..d73238d 100644 --- a/src/jwebmail/templates/login.html +++ b/src/jwebmail/templates/login.html @@ -26,7 +26,7 @@ </div> {% endif %} - <form name="login1" method="POST" class="pure-form pure-form-aligned jwm-round"> + <form name="login1" id="login1" method="POST" class="pure-form pure-form-aligned jwm-round"> {{ login_form.csrf_token }} @@ -74,10 +74,10 @@ {% block scripts %} <script type="text/javascript"> - if (!document.login1.userid.value) { - document.login1.userid.focus(); + if (!document.forms.login1.username.value) { + document.forms.login1.username.focus(); } else { - document.login1.password.focus(); + document.forms.login1.password.focus(); } </script> {% endblock %} diff --git a/src/jwebmail/translations/de/LC_MESSAGES/messages.po b/src/jwebmail/translations/de/LC_MESSAGES/messages.po index ad48d16..48b1dca 100644 --- a/src/jwebmail/translations/de/LC_MESSAGES/messages.po +++ b/src/jwebmail/translations/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-11-26 19:21+0100\n" +"POT-Creation-Date: 2023-12-04 22:59+0100\n" "PO-Revision-Date: 2023-11-23 12:18+0100\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language: de\n" @@ -18,150 +18,160 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.13.1\n" -#: jwebmail/render_mail.py:63 +#: src/jwebmail/render_mail.py:67 msgid "Attachment {filename} of type {filetype}" msgstr "Anhang {filename} des types {filetype}" -#: jwebmail/render_mail.py:113 +#: src/jwebmail/render_mail.py:117 msgid "From" msgstr "Von" -#: jwebmail/render_mail.py:114 +#: src/jwebmail/render_mail.py:118 msgid "To" -msgstr "Fuer" +msgstr "Für" -#: jwebmail/render_mail.py:115 +#: src/jwebmail/render_mail.py:119 msgid "CC" msgstr "" -#: jwebmail/render_mail.py:116 +#: src/jwebmail/render_mail.py:120 msgid "BCC" msgstr "" -#: jwebmail/webmail.py:39 +#: src/jwebmail/render_mail.py:147 +msgid "Unsupported MIME type of {mime_type}." +msgstr "Nicht unterstützter MIME-Typ {mime_type}." + +#: src/jwebmail/webmail.py:27 msgid "Username" msgstr "Nutzername" -#: jwebmail/webmail.py:41 +#: src/jwebmail/webmail.py:29 msgid "Password" msgstr "Passwort" -#: jwebmail/webmail.py:75 -msgid "login failed" -msgstr "Login fehlgeschlagen" +#: src/jwebmail/webmail.py:64 +msgid "login failed!" +msgstr "Login fehlgeschlagen!" -#: jwebmail/webmail.py:120 +#: src/jwebmail/webmail.py:111 msgid "displaying <b>{start} - {end}</b> of <b>{total}</b> {record_name}" msgstr "zeige <b>{start} - {end}</b> von <b>{total}</b> {record_name} an" -#: jwebmail/webmail.py:173 +#: src/jwebmail/webmail.py:164 msgid "succ_move" msgstr "erfolgreich Verschoben" -#: jwebmail/webmail.py:221 +#: src/jwebmail/webmail.py:211 msgid "error_send" msgstr "Fehler beim senden" -#: jwebmail/webmail.py:223 +#: src/jwebmail/webmail.py:213 msgid "succ_send" msgstr "erfolgreich Verschoben" -#: jwebmail/templates/_bot_nav.html:11 +#: src/jwebmail/templates/_bot_nav.html:11 msgid "Move to" msgstr "Verschiebe nach" -#: jwebmail/templates/_bot_nav.html:19 jwebmail/templates/_folders.html:21 +#: src/jwebmail/templates/_bot_nav.html:19 +#: src/jwebmail/templates/_folders.html:21 msgid "Home" msgstr "Ursprung" -#: jwebmail/templates/_bot_nav.html:26 +#: src/jwebmail/templates/_bot_nav.html:26 msgid "Move" msgstr "Verschieben" -#: jwebmail/templates/_bot_nav.html:38 +#: src/jwebmail/templates/_bot_nav.html:38 msgid "Remove" -msgstr "Loeschen" +msgstr "Löschen" -#: jwebmail/templates/_bot_nav.html:44 +#: src/jwebmail/templates/_bot_nav.html:44 msgid "check all" msgstr "alle markieren" -#: jwebmail/templates/_folders.html:36 +#: src/jwebmail/templates/_folders.html:36 #, python-format msgid "%(total_new_mails)s new" msgstr "%(total_new_mails)s neu" -#: jwebmail/templates/_folders.html:41 +#: src/jwebmail/templates/_folders.html:41 msgid "mailbox size: " -msgstr "mailbox groesse: " +msgstr "mailbox größe: " -#: jwebmail/templates/_top_nav.html:4 +#: src/jwebmail/templates/_top_nav.html:4 msgid "Logout" msgstr "Abmelden" -#: jwebmail/templates/_top_nav.html:5 +#: src/jwebmail/templates/_top_nav.html:5 msgid "Write" msgstr "Schreiben" -#: jwebmail/templates/_top_nav.html:11 +#: src/jwebmail/templates/_top_nav.html:11 msgid "Search" msgstr "Suchen" -#: jwebmail/templates/_top_nav.html:26 +#: src/jwebmail/templates/_top_nav.html:26 msgid "Sort" msgstr "Sortieren" -#: jwebmail/templates/_top_nav.html:32 jwebmail/templates/_top_nav.html:33 +#: src/jwebmail/templates/_top_nav.html:32 +#: src/jwebmail/templates/_top_nav.html:33 msgid "Date" msgstr "Datum" -#: jwebmail/templates/_top_nav.html:32 jwebmail/templates/_top_nav.html:34 -#: jwebmail/templates/_top_nav.html:35 +#: src/jwebmail/templates/_top_nav.html:32 +#: src/jwebmail/templates/_top_nav.html:34 +#: src/jwebmail/templates/_top_nav.html:35 msgid "Descending" msgstr "Absteigend" -#: jwebmail/templates/_top_nav.html:33 jwebmail/templates/_top_nav.html:36 +#: src/jwebmail/templates/_top_nav.html:33 +#: src/jwebmail/templates/_top_nav.html:36 msgid "Ascending" msgstr "Aufsteigend" -#: jwebmail/templates/_top_nav.html:34 +#: src/jwebmail/templates/_top_nav.html:34 msgid "Size" -msgstr "Groesse" +msgstr "Größe" -#: jwebmail/templates/_top_nav.html:35 jwebmail/templates/_top_nav.html:36 +#: src/jwebmail/templates/_top_nav.html:35 +#: src/jwebmail/templates/_top_nav.html:36 msgid "Sender" msgstr "Sender" -#: jwebmail/templates/about.html:57 jwebmail/templates/login.html:13 -#: jwebmail/templates/login.html:69 +#: src/jwebmail/templates/about.html:61 src/jwebmail/templates/login.html:8 +#: src/jwebmail/templates/login.html:64 msgid "Login" msgstr "Anmelden" -#: jwebmail/templates/displayheaders.html:25 +#: src/jwebmail/templates/displayheaders.html:25 msgid "This folder is empty!" msgstr "Dieses Verzeichnis ist leer!" -#: jwebmail/templates/mainlayout.html:22 +#: src/jwebmail/templates/mainlayout.html:22 msgid "About" -msgstr "Ueber" +msgstr "Über" -#: jwebmail/templates/mainlayout.html:25 +#: src/jwebmail/templates/mainlayout.html:25 msgid "Version" msgstr "Version" -#: jwebmail/templates/not_found.html:19 +#: src/jwebmail/templates/not_found.html:19 msgid "start page" msgstr "Startseite" -#: jwebmail/templates/readmail.html:12 jwebmail/templates/writemail.html:78 +#: src/jwebmail/templates/readmail.html:12 +#: src/jwebmail/templates/writemail.html:78 msgid "back" -msgstr "zurueck" +msgstr "zurück" -#: jwebmail/templates/writemail.html:62 +#: src/jwebmail/templates/writemail.html:62 msgid "attach file" -msgstr "Datei anhaengen" +msgstr "Datei anhängen" -#: jwebmail/templates/writemail.html:71 +#: src/jwebmail/templates/writemail.html:71 msgid "Send" msgstr "Senden" diff --git a/src/jwebmail/webmail.py b/src/jwebmail/webmail.py index 4e47dbd..2cfc834 100644 --- a/src/jwebmail/webmail.py +++ b/src/jwebmail/webmail.py @@ -2,7 +2,7 @@ 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 UserMixin, current_user, login_user, logout_user +from flask_login import current_user, login_user, logout_user from flask_paginate import Pagination, get_page_parameter, get_per_page_parameter from flask_wtf import FlaskForm from wtforms import ( @@ -16,15 +16,11 @@ from wtforms import ( ) from .model.read_mails import QMAuthError -from .read_mails import add_user, get_read_mails_logged_in +from .read_mails import JWebmailUser, add_user, get_read_mails_logged_in from .read_mails import login as rm_login from .render_mail import to_mime_type - -class JWebmailUser(UserMixin): - def __init__(self, mail_addr, password): - self.id = mail_addr - self.password = password +DEFAULT_LANGUAGE = "de" class LoginForm(FlaskForm): @@ -51,6 +47,7 @@ def login(): form = LoginForm() warn = "" + rc = 200 if form.validate_on_submit(): if rm_login(form.username.data, form.password.data): @@ -58,15 +55,17 @@ def login(): add_user(user) login_user(user) - next = request.args.get("next") + nxt = request.args.get("next") - if urlparse(next).netloc: + if urlparse(nxt).netloc: abort(401) - return redirect(next or url_for("displayheaders"), 303) + return redirect(nxt or url_for("displayheaders"), 303) else: - warn = gettext("login failed") + warn = gettext("login failed!") + elif request.method == "POST": + rc = 401 - return render_template("login.html", login_form=form, warn=warn), 401 + return render_template("login.html", login_form=form, warn=warn), rc def logout(): @@ -137,13 +136,13 @@ def displayheaders(folder=""): return render_template("displayheaders.html", **vals) -def readmail(msgid): +def readmail(msgid, folder=""): try: - mail = get_read_mails_logged_in().show("", msgid) + mail = get_read_mails_logged_in().show(folder, msgid) except QMAuthError: return render_template("not_found.html"), 404 - return render_template("readmail.html", msg=mail) + return render_template("readmail.html", msg=mail, folder=folder) def writemail(): @@ -166,10 +165,10 @@ def move(folder): return redirect(url_for("displayheaders"), 303) -def rawmail(msgid): +def rawmail(msgid, folder=""): path = request.args.get("path", "") - content = get_read_mails_logged_in().raw("", msgid, path) + content = get_read_mails_logged_in().raw(folder, msgid, path) headers = [] @@ -213,109 +212,3 @@ def sendmail(): flash(gettext("succ_send")) return redirect(url_for("displayheaders"), 303) - - -""" -sub remove { - my $self = shift; - - my $v = $self->validation; - $v->csrf_protect; - $v->required('mail'); - - if ($v->has_error) { - $self->reply->exception('errors in ' . join('', $v->failed->@*)); - return; - } - - my $auth = $self->stash(STS_AUTH); - - my $mm = $self->every_param('mail'); - my $folder = $self->stash('folder'); - - $self->users->remove($auth, $folder, $_) for @$mm; - - $self->res->code(303); - $self->redirect_to('displayheaders'); -} - - -### session password handling - -use constant { S_PASSWD => 'pw', S_OTP_S3D_PW => 'otp_s3d_pw' }; - -sub _rand_data { - my $len = shift; - - if (TRUE_RANDOM) { - #return makerandom_octet(Length => $len, Strength => 0); # was used for Crypt::Random - return urandom($len); - } - else { - my $res = ''; - for (0..$len-1) { - vec($res, $_, 8) = int rand 256; - } - - return $res; - } -} - -sub _session_passwd { - my ($self, $passwd, $challenge) = @_; - my $secAlg = LOGIN_SCHEME; - - $self->_warn_crypt; - - if (defined $passwd) { # set - if ($secAlg eq fc 'cram_md5') { - $self->session(S_PASSWD() => $passwd, challenge => $challenge); - } - elsif ($secAlg eq fc 'plain') { - unless ($passwd) { - $self->s3d(S_PASSWD, ''); - delete $self->session->{S_OTP_S3D_PW()}; - return; - } - die "'$passwd' contains invalid character \\n" if $passwd =~ /\n/; - if (length $passwd < 20) { - $passwd .= "\n" . ' ' x (20 - length($passwd) - 1); - } - my $passwd_utf8 = encode('UTF-8', $passwd); - my $rand_bytes = _rand_data(length $passwd_utf8); - $self->s3d(S_PASSWD, b64_encode($passwd_utf8 ^ $rand_bytes, '')); - $self->session(S_OTP_S3D_PW, b64_encode($rand_bytes, '')); - } - else { - die - } - } - else { # get - if ($secAlg eq fc 'cram_md5') { - wantarray or carp "you forgot the challenge"; - return ($self->session(S_PASSWD), $self->session('challenge')); - } - elsif ($secAlg eq fc 'plain') { - my $pw = b64_decode($self->s3d(S_PASSWD) || ''); - my $otp = b64_decode($self->session(S_OTP_S3D_PW) || ''); - my ($res) = split "\n", decode('UTF-8', $pw ^ $otp), 2; - return $res; - } - else { - die - } - } -} - -sub _warn_crypt { - my $self = shift; - - state $once = 0; - - if ( !TRUE_RANDOM && !$once && LOGIN_SCHEME eq fc 'plain' ) { - $self->log->warn("Falling back to pseudo random generation. Please install Crypt::URandom"); - $once = 1; - } -} - -""" |