Repository: Mailu/Mailu

Issue:
ADMIN: Autentication is locked if error is thrown
## Environment & Versions
### Environment
 - [x] docker-compose
 - [ ] kubernetes
 - [ ] docker swarm

### Versions
To find your version, get the image name of a mailu container and read  the version from the tag (example for version 1.7).
```
1.9.23
```

## Description
The when the basic auth api throw an error make nginx enter an "invalid" state where it think the it has successfully auteticated.
a thorw-catch block would solve this issue.

## Replication Steps
- Access `https://mail.example.com/webdav/` and login with username only (no `@example.com`)
- Now you cannot "log out". The page is completly stuck. Clearing cache/cookie does nothing.

## Expected behaviour
I would expect an auth rejection. So I can properly login with the right user.

## Logs
```
[2022-03-26 15:48:38,308] ERROR in app: Exception on /internal/auth/basic [GET]
Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1699, in _execute_context
    context = constructor(
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 1076, in _init_compiled
    param = {
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 1077, in <dictcomp>
    key: processors[key](compiled_params[key])
  File "/usr/lib/python3.9/site-packages/sqlalchemy/sql/type_api.py", line 1379, in process
    return process_param(value, dialect)
  File "/app/mailu/models.py", line 63, in process_bind_param
    raise ValueError('invalid email address (no "@")')
ValueError: invalid email address (no "@")
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
  File "/usr/lib/python3.9/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/lib/python3.9/site-packages/flask/app.py", line 1518, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/lib/python3.9/site-packages/flask/app.py", line 1516, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/lib/python3.9/site-packages/flask/app.py", line 1502, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
  File "/app/mailu/internal/views/auth.py", line 99, in basic_authentication
    user = models.User.query.get(user_email)
  File "<string>", line 2, in get
  File "/usr/lib/python3.9/site-packages/sqlalchemy/util/deprecations.py", line 390, in warned
    return fn(*args, **kwargs)
  File "/usr/lib/python3.9/site-packages/sqlalchemy/orm/query.py", line 943, in get
    return self._get_impl(ident, loading.load_on_pk_identity)
  File "/usr/lib/python3.9/site-packages/sqlalchemy/orm/query.py", line 947, in _get_impl
    return self.session._get_impl(
  File "/usr/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 2869, in _get_impl
    return db_load_fn(
  File "/usr/lib/python3.9/site-packages/sqlalchemy/orm/loading.py", line 524, in load_on_pk_identity
    session.execute(
  File "/usr/lib/python3.9/site-packages/sqlalchemy/orm/session.py", line 1689, in execute
    result = conn._execute_20(statement, params or {}, execution_options)
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1611, in _execute_20
    return meth(self, args_10style, kwargs_10style, execution_options)
  File "/usr/lib/python3.9/site-packages/sqlalchemy/sql/elements.py", line 325, in _execute_on_connection
    return connection._execute_clauseelement(
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1478, in _execute_clauseelement
    ret = self._execute_context(
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1705, in _execute_context
    self._handle_dbapi_exception(
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 2023, in _handle_dbapi_exception
    util.raise_(
  File "/usr/lib/python3.9/site-packages/sqlalchemy/util/compat.py", line 207, in raise_
    raise exception
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1699, in _execute_context
    context = constructor(
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 1076, in _init_compiled
    param = {
  File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 1077, in <dictcomp>
    key: processors[key](compiled_params[key])
  File "/usr/lib/python3.9/site-packages/sqlalchemy/sql/type_api.py", line 1379, in process
    return process_param(value, dialect)
  File "/app/mailu/models.py", line 63, in process_bind_param
    raise ValueError('invalid email address (no "@")')
sqlalchemy.exc.StatementError: (builtins.ValueError) invalid email address (no "@")

[SQL: SELECT "user".email AS user_email, "user".created_at AS user_created_at, "user".updated_at AS user_updated_at, "user".comment AS user_comment, "user".localpart AS user_localpart, "user".password AS user_password, "user".quota_bytes AS user_quota_bytes, "user".quota_bytes_used AS user_quota_bytes_used, "user".global_admin AS user_global_admin, "user".enabled AS user_enabled, "user".enable_imap AS user_enable_imap, "user".enable_pop AS user_enable_pop, "user".forward_enabled AS user_forward_enabled, "user".forward_destination AS user_forward_destination, "user".forward_keep AS user_forward_keep, "user".reply_enabled AS user_reply_enabled, "user".reply_subject AS user_reply_subject, "user".reply_body AS user_reply_body, "user".reply_startdate AS user_reply_startdate, "user".reply_enddate AS user_reply_enddate, "user".displayed_name AS user_displayed_name, "user".spam_enabled AS user_spam_enabled, "user".spam_threshold AS user_spam_threshold, "user".domain_name AS user_domain_name 
```



Discussion:
Odds are it's something with your user-agent... nginx should eventually reply with something (most likely an HTTP 400)

Does browsing to https://nobody@mail.example.com/webdav/ fix it?

Resolving patch:
diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py
--- a/core/admin/mailu/internal/views/auth.py
+++ b/core/admin/mailu/internal/views/auth.py
@@ -5,6 +5,7 @@
 import flask
 import flask_login
 import base64
+import sqlalchemy.exc
 
 @internal.route("/auth/email")
 def nginx_authentication():
@@ -96,13 +97,19 @@ def basic_authentication():
             response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit for this username exceeded"'
             response.headers['Retry-After'] = '60'
             return response
-        user = models.User.query.get(user_email)
-        if user and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"):
-            response = flask.Response()
-            response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "")
-            utils.limiter.exempt_ip_from_ratelimits(client_ip)
-            return response
-        utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)
+        try:
+            user = models.User.query.get(user_email) if '@' in user_email else None
+        except sqlalchemy.exc.StatementError as exc:
+            exc = str(exc).split('\n', 1)[0]
+            app.logger.warn(f'Invalid user {user_email!r}: {exc}')
+        else:
+            if user is not None and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"):
+                response = flask.Response()
+                response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "")
+                utils.limiter.exempt_ip_from_ratelimits(client_ip)
+                return response
+            # We failed check_credentials
+            utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)
     response = flask.Response(status=401)
     response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'
     return response


Test patch: