We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
AUTH_RATELIMIT_IP not working on imap/pop3/smtp
## Environment & Versions
### Environment
 - [x] docker-compose
 - [x] kubernetes
 - [x] docker swarm (not tested)

### Versions
- 1.9

## Description
Currently, `AUTH_RATELIMIT_IP` seems to have no effect when logging in using imap/pop3/smtp.

## Replication Steps
1. Start a new (local) mailu server (no certs needed) using the web generator and set `AUTH_RATELIMIT_IP` to `3/hour`.
2. `telnet localhost 143` -> `login nobody@example.com PASSWORD`
3. repeat step 2 three times.

## Expected behaviour
On the last login, the server should respond with `. NO Temporary authentication failure (rate-limit)`.
Instead, it's possible to to repeat the login attempt, until `AUTH_RATELIMIT_USER` hits. 

## Additional information
When the user gets into `AUTH_RATELIMIT_IP` using the web interface, imap/pop3/smtp login attempts are stopped as expected.

## Logs
```
❯ telnet localhost 143
Telnet escape character is '^Q'.
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^Q'.
* OK IMAP4 ready
. login nobody@example.com PASSWORD
. NO Authentication credentials invalid
Connection closed by foreign host.
❯ telnet localhost 143
Telnet escape character is '^Q'.
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^Q'.
* OK IMAP4 ready
. login nobody@example.com PASSWORD
. NO Authentication credentials invalid
Connection closed by foreign host.
❯ telnet localhost 143
Telnet escape character is '^Q'.
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^Q'.
* OK IMAP4 ready
. login nobody@example.com PASSWORD
. NO Authentication credentials invalid
Connection closed by foreign host.
❯ telnet localhost 143
Telnet escape character is '^Q'.
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^Q'.
* OK IMAP4 ready
. login nobody@example.com PASSWORD
. NO Authentication credentials invalid
Connection closed by foreign host.
```

I'll provide a PR.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `core/admin/mailu/internal/nginx.py`
Content:
```
1 from mailu import models, utils
2 from flask import current_app as app
3 
4 import re
5 import urllib
6 import ipaddress
7 import socket
8 import sqlalchemy.exc
9 import tenacity
10 
11 SUPPORTED_AUTH_METHODS = ["none", "plain"]
12 
13 
14 STATUSES = {
15     "authentication": ("Authentication credentials invalid", {
16         "imap": "AUTHENTICATIONFAILED",
17         "smtp": "535 5.7.8",
18         "pop3": "-ERR Authentication failed"
19     }),
20     "encryption": ("Must issue a STARTTLS command first", {
21         "smtp": "530 5.7.0"
22     }),
23     "ratelimit": ("Temporary authentication failure (rate-limit)", {
24         "imap": "LIMIT",
25         "smtp": "451 4.3.2",
26         "pop3": "-ERR [LOGIN-DELAY] Retry later"
27     }),
28 }
29 
30 def check_credentials(user, password, ip, protocol=None, auth_port=None):
31     if not user or not user.enabled or (protocol == "imap" and not user.enable_imap) or (protocol == "pop3" and not user.enable_pop):
32         return False
33     is_ok = False
34     # webmails
35     if auth_port in ['10143', '10025'] and password.startswith('token-'):
36         if utils.verify_temp_token(user.get_id(), password):
37             is_ok = True
38     # All tokens are 32 characters hex lowercase
39     if not is_ok and len(password) == 32:
40         for token in user.tokens:
41             if (token.check_password(password) and
42                 (not token.ip or token.ip == ip)):
43                     is_ok = True
44                     break
45     if not is_ok and user.check_password(password):
46         is_ok = True
47     return is_ok
48 
49 def handle_authentication(headers):
50     """ Handle an HTTP nginx authentication request
51     See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol
52     """
53     method = headers["Auth-Method"]
54     protocol = headers["Auth-Protocol"]
55     # Incoming mail, no authentication
56     if method == "none" and protocol == "smtp":
57         server, port = get_server(protocol, False)
58         if app.config["INBOUND_TLS_ENFORCE"]:
59             if "Auth-SSL" in headers and headers["Auth-SSL"] == "on":
60                 return {
61                     "Auth-Status": "OK",
62                     "Auth-Server": server,
63                     "Auth-Port": port
64                 }
65             else:
66                 status, code = get_status(protocol, "encryption")
67                 return {
68                     "Auth-Status": status,
69                     "Auth-Error-Code" : code,
70                     "Auth-Wait": 0
71                 }
72         else:
73             return {
74                 "Auth-Status": "OK",
75                 "Auth-Server": server,
76                 "Auth-Port": port
77             }
78     # Authenticated user
79     elif method == "plain":
80         is_valid_user = False
81         # According to RFC2616 section 3.7.1 and PEP 3333, HTTP headers should
82         # be ASCII and are generally considered ISO8859-1. However when passing
83         # the password, nginx does not transcode the input UTF string, thus
84         # we need to manually decode.
85         raw_user_email = urllib.parse.unquote(headers["Auth-User"])
86         raw_password = urllib.parse.unquote(headers["Auth-Pass"])
87         user_email = 'invalid'
88         try:
89             user_email = raw_user_email.encode("iso8859-1").decode("utf8")
90             password = raw_password.encode("iso8859-1").decode("utf8")
91             ip = urllib.parse.unquote(headers["Client-Ip"])
92         except:
93             app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}')
94         else:
95             try:
96                 user = models.User.query.get(user_email) if '@' in user_email else None
97                 is_valid_user = user is not None
98             except sqlalchemy.exc.StatementError as exc:
99                 exc = str(exc).split('\n', 1)[0]
100                 app.logger.warn(f'Invalid user {user_email!r}: {exc}')
101             else:
102                 ip = urllib.parse.unquote(headers["Client-Ip"])
103                 if check_credentials(user, password, ip, protocol, headers["Auth-Port"]):
104                     server, port = get_server(headers["Auth-Protocol"], True)
105                     return {
106                         "Auth-Status": "OK",
107                         "Auth-Server": server,
108                         "Auth-User": user_email,
109                         "Auth-User-Exists": is_valid_user,
110                         "Auth-Port": port
111                     }
112         status, code = get_status(protocol, "authentication")
113         return {
114             "Auth-Status": status,
115             "Auth-Error-Code": code,
116             "Auth-User": user_email,
117             "Auth-User-Exists": is_valid_user,
118             "Auth-Wait": 0
119         }
120     # Unexpected
121     return {}
122 
123 
124 def get_status(protocol, status):
125     """ Return the proper error code depending on the protocol
126     """
127     status, codes = STATUSES[status]
128     return status, codes[protocol]
129 
130 def extract_host_port(host_and_port, default_port):
131     host, _, port = re.match('^(.*?)(:([0-9]*))?$', host_and_port).groups()
132     return host, int(port) if port else default_port
133 
134 def get_server(protocol, authenticated=False):
135     if protocol == "imap":
136         hostname, port = extract_host_port(app.config['IMAP_ADDRESS'], 143)
137     elif protocol == "pop3":
138         hostname, port = extract_host_port(app.config['POP3_ADDRESS'], 110)
139     elif protocol == "smtp":
140         if authenticated:
141             hostname, port = extract_host_port(app.config['AUTHSMTP_ADDRESS'], 10025)
142         else:
143             hostname, port = extract_host_port(app.config['SMTP_ADDRESS'], 25)
144     try:
145         # test if hostname is already resolved to an ip adddress
146         ipaddress.ip_address(hostname)
147     except:
148         # hostname is not an ip address - so we need to resolve it
149         hostname = resolve_hostname(hostname)
150     return hostname, port
151 
152 @tenacity.retry(stop=tenacity.stop_after_attempt(100),
153                 wait=tenacity.wait_random(min=2, max=5))
154 def resolve_hostname(hostname):
155     """ This function uses system DNS to resolve a hostname.
156     It is capable of retrying in case the host is not immediately available
157     """
158     return socket.gethostbyname(hostname)
159
```

Path: `core/admin/mailu/internal/views/auth.py`
Content:
```
1 from mailu import models, utils
2 from mailu.internal import internal, nginx
3 from flask import current_app as app
4 
5 import flask
6 import flask_login
7 import base64
8 
9 @internal.route("/auth/email")
10 def nginx_authentication():
11     """ Main authentication endpoint for Nginx email server
12     """
13     client_ip = flask.request.headers["Client-Ip"]
14     headers = flask.request.headers
15     if headers["Auth-Port"] == '25' and headers['Auth-Method'] != 'none':
16         response = flask.Response()
17         response.headers['Auth-Status'] = 'AUTH not supported'
18         response.headers['Auth-Error-Code'] = '502 5.5.1'
19         utils.limiter.rate_limit_ip(client_ip)
20         return response
21     is_from_webmail = headers['Auth-Port'] in ['10143', '10025']
22     if not is_from_webmail and utils.limiter.should_rate_limit_ip(client_ip):
23         status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')
24         response = flask.Response()
25         response.headers['Auth-Status'] = status
26         response.headers['Auth-Error-Code'] = code
27         if int(flask.request.headers['Auth-Login-Attempt']) < 10:
28             response.headers['Auth-Wait'] = '3'
29         return response
30     headers = nginx.handle_authentication(flask.request.headers)
31     response = flask.Response()
32     for key, value in headers.items():
33         response.headers[key] = str(value)
34     is_valid_user = False
35     if response.headers.get("Auth-User-Exists"):
36         username = response.headers["Auth-User"]
37         if utils.limiter.should_rate_limit_user(username, client_ip):
38             # FIXME could be done before handle_authentication()
39             status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')
40             response = flask.Response()
41             response.headers['Auth-Status'] = status
42             response.headers['Auth-Error-Code'] = code
43             if int(flask.request.headers['Auth-Login-Attempt']) < 10:
44                 response.headers['Auth-Wait'] = '3'
45             return response
46         is_valid_user = True
47     if headers.get("Auth-Status") == "OK":
48         utils.limiter.exempt_ip_from_ratelimits(client_ip)
49     elif is_valid_user:
50         utils.limiter.rate_limit_user(username, client_ip)
51     elif not is_from_webmail:
52         utils.limiter.rate_limit_ip(client_ip)
53     return response
54 
55 @internal.route("/auth/admin")
56 def admin_authentication():
57     """ Fails if the user is not an authenticated admin.
58     """
59     if (not flask_login.current_user.is_anonymous
60         and flask_login.current_user.global_admin
61         and flask_login.current_user.enabled):
62         return ""
63     return flask.abort(403)
64 
65 @internal.route("/auth/user")
66 def user_authentication():
67     """ Fails if the user is not authenticated.
68     """
69     if (not flask_login.current_user.is_anonymous
70         and flask_login.current_user.enabled):
71         response = flask.Response()
72         email = flask_login.current_user.get_id()
73         response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, email, "")
74         response.headers["X-User-Token"] = utils.gen_temp_token(email, flask.session)
75         return response
76     return flask.abort(403)
77 
78 
79 @internal.route("/auth/basic")
80 def basic_authentication():
81     """ Tries to authenticate using the Authorization header.
82     """
83     client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)
84     if utils.limiter.should_rate_limit_ip(client_ip):
85         response = flask.Response(status=401)
86         response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit from one source exceeded"'
87         response.headers['Retry-After'] = '60'
88         return response
89     authorization = flask.request.headers.get("Authorization")
90     if authorization and authorization.startswith("Basic "):
91         encoded = authorization.replace("Basic ", "")
92         user_email, password = base64.b64decode(encoded).split(b":", 1)
93         user_email = user_email.decode("utf8")
94         if utils.limiter.should_rate_limit_user(user_email, client_ip):
95             response = flask.Response(status=401)
96             response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit for this username exceeded"'
97             response.headers['Retry-After'] = '60'
98             return response
99         user = models.User.query.get(user_email)
100         if user and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"):
101             response = flask.Response()
102             response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "")
103             utils.limiter.exempt_ip_from_ratelimits(client_ip)
104             return response
105         utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)
106     response = flask.Response(status=401)
107     response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"'
108     return response
109
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py
--- a/core/admin/mailu/internal/nginx.py
+++ b/core/admin/mailu/internal/nginx.py
@@ -94,11 +94,11 @@
         else:
             try:
                 user = models.User.query.get(user_email) if '@' in user_email else None
-                is_valid_user = user is not 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:
+                is_valid_user = user is not None
                 ip = urllib.parse.unquote(headers["Client-Ip"])
                 if check_credentials(user, password, ip, protocol, headers["Auth-Port"]):
                     server, port = get_server(headers["Auth-Protocol"], True)
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
@@ -32,7 +32,7 @@
     for key, value in headers.items():
         response.headers[key] = str(value)
     is_valid_user = False
-    if response.headers.get("Auth-User-Exists"):
+    if response.headers.get("Auth-User-Exists") == "True":
         username = response.headers["Auth-User"]
         if utils.limiter.should_rate_limit_user(username, client_ip):
             # FIXME could be done before handle_authentication()

{"golden_diff": "diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py\n--- a/core/admin/mailu/internal/nginx.py\n+++ b/core/admin/mailu/internal/nginx.py\n@@ -94,11 +94,11 @@\n         else:\n             try:\n                 user = models.User.query.get(user_email) if '@' in user_email else None\n-                is_valid_user = user is not None\n             except sqlalchemy.exc.StatementError as exc:\n                 exc = str(exc).split('\\n', 1)[0]\n                 app.logger.warn(f'Invalid user {user_email!r}: {exc}')\n             else:\n+                is_valid_user = user is not None\n                 ip = urllib.parse.unquote(headers[\"Client-Ip\"])\n                 if check_credentials(user, password, ip, protocol, headers[\"Auth-Port\"]):\n                     server, port = get_server(headers[\"Auth-Protocol\"], True)\ndiff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py\n--- a/core/admin/mailu/internal/views/auth.py\n+++ b/core/admin/mailu/internal/views/auth.py\n@@ -32,7 +32,7 @@\n     for key, value in headers.items():\n         response.headers[key] = str(value)\n     is_valid_user = False\n-    if response.headers.get(\"Auth-User-Exists\"):\n+    if response.headers.get(\"Auth-User-Exists\") == \"True\":\n         username = response.headers[\"Auth-User\"]\n         if utils.limiter.should_rate_limit_user(username, client_ip):\n             # FIXME could be done before handle_authentication()\n", "issue": "AUTH_RATELIMIT_IP not working on imap/pop3/smtp\n## Environment & Versions\r\n### Environment\r\n - [x] docker-compose\r\n - [x] kubernetes\r\n - [x] docker swarm (not tested)\r\n\r\n### Versions\r\n- 1.9\r\n\r\n## Description\r\nCurrently, `AUTH_RATELIMIT_IP` seems to have no effect when logging in using imap/pop3/smtp.\r\n\r\n## Replication Steps\r\n1. Start a new (local) mailu server (no certs needed) using the web generator and set `AUTH_RATELIMIT_IP` to `3/hour`.\r\n2. `telnet localhost 143` -> `login nobody@example.com PASSWORD`\r\n3. repeat step 2 three times.\r\n\r\n## Expected behaviour\r\nOn the last login, the server should respond with `. NO Temporary authentication failure (rate-limit)`.\r\nInstead, it's possible to to repeat the login attempt, until `AUTH_RATELIMIT_USER` hits. \r\n\r\n## Additional information\r\nWhen the user gets into `AUTH_RATELIMIT_IP` using the web interface, imap/pop3/smtp login attempts are stopped as expected.\r\n\r\n## Logs\r\n```\r\n\u276f telnet localhost 143\r\nTelnet escape character is '^Q'.\r\nTrying 127.0.0.1...\r\nConnected to localhost.\r\nEscape character is '^Q'.\r\n* OK IMAP4 ready\r\n. login nobody@example.com PASSWORD\r\n. NO Authentication credentials invalid\r\nConnection closed by foreign host.\r\n\u276f telnet localhost 143\r\nTelnet escape character is '^Q'.\r\nTrying 127.0.0.1...\r\nConnected to localhost.\r\nEscape character is '^Q'.\r\n* OK IMAP4 ready\r\n. login nobody@example.com PASSWORD\r\n. NO Authentication credentials invalid\r\nConnection closed by foreign host.\r\n\u276f telnet localhost 143\r\nTelnet escape character is '^Q'.\r\nTrying 127.0.0.1...\r\nConnected to localhost.\r\nEscape character is '^Q'.\r\n* OK IMAP4 ready\r\n. login nobody@example.com PASSWORD\r\n. NO Authentication credentials invalid\r\nConnection closed by foreign host.\r\n\u276f telnet localhost 143\r\nTelnet escape character is '^Q'.\r\nTrying 127.0.0.1...\r\nConnected to localhost.\r\nEscape character is '^Q'.\r\n* OK IMAP4 ready\r\n. login nobody@example.com PASSWORD\r\n. NO Authentication credentials invalid\r\nConnection closed by foreign host.\r\n```\r\n\r\nI'll provide a PR.\n", "before_files": [{"content": "from mailu import models, utils\nfrom flask import current_app as app\n\nimport re\nimport urllib\nimport ipaddress\nimport socket\nimport sqlalchemy.exc\nimport tenacity\n\nSUPPORTED_AUTH_METHODS = [\"none\", \"plain\"]\n\n\nSTATUSES = {\n    \"authentication\": (\"Authentication credentials invalid\", {\n        \"imap\": \"AUTHENTICATIONFAILED\",\n        \"smtp\": \"535 5.7.8\",\n        \"pop3\": \"-ERR Authentication failed\"\n    }),\n    \"encryption\": (\"Must issue a STARTTLS command first\", {\n        \"smtp\": \"530 5.7.0\"\n    }),\n    \"ratelimit\": (\"Temporary authentication failure (rate-limit)\", {\n        \"imap\": \"LIMIT\",\n        \"smtp\": \"451 4.3.2\",\n        \"pop3\": \"-ERR [LOGIN-DELAY] Retry later\"\n    }),\n}\n\ndef check_credentials(user, password, ip, protocol=None, auth_port=None):\n    if not user or not user.enabled or (protocol == \"imap\" and not user.enable_imap) or (protocol == \"pop3\" and not user.enable_pop):\n        return False\n    is_ok = False\n    # webmails\n    if auth_port in ['10143', '10025'] and password.startswith('token-'):\n        if utils.verify_temp_token(user.get_id(), password):\n            is_ok = True\n    # All tokens are 32 characters hex lowercase\n    if not is_ok and len(password) == 32:\n        for token in user.tokens:\n            if (token.check_password(password) and\n                (not token.ip or token.ip == ip)):\n                    is_ok = True\n                    break\n    if not is_ok and user.check_password(password):\n        is_ok = True\n    return is_ok\n\ndef handle_authentication(headers):\n    \"\"\" Handle an HTTP nginx authentication request\n    See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol\n    \"\"\"\n    method = headers[\"Auth-Method\"]\n    protocol = headers[\"Auth-Protocol\"]\n    # Incoming mail, no authentication\n    if method == \"none\" and protocol == \"smtp\":\n        server, port = get_server(protocol, False)\n        if app.config[\"INBOUND_TLS_ENFORCE\"]:\n            if \"Auth-SSL\" in headers and headers[\"Auth-SSL\"] == \"on\":\n                return {\n                    \"Auth-Status\": \"OK\",\n                    \"Auth-Server\": server,\n                    \"Auth-Port\": port\n                }\n            else:\n                status, code = get_status(protocol, \"encryption\")\n                return {\n                    \"Auth-Status\": status,\n                    \"Auth-Error-Code\" : code,\n                    \"Auth-Wait\": 0\n                }\n        else:\n            return {\n                \"Auth-Status\": \"OK\",\n                \"Auth-Server\": server,\n                \"Auth-Port\": port\n            }\n    # Authenticated user\n    elif method == \"plain\":\n        is_valid_user = False\n        # According to RFC2616 section 3.7.1 and PEP 3333, HTTP headers should\n        # be ASCII and are generally considered ISO8859-1. However when passing\n        # the password, nginx does not transcode the input UTF string, thus\n        # we need to manually decode.\n        raw_user_email = urllib.parse.unquote(headers[\"Auth-User\"])\n        raw_password = urllib.parse.unquote(headers[\"Auth-Pass\"])\n        user_email = 'invalid'\n        try:\n            user_email = raw_user_email.encode(\"iso8859-1\").decode(\"utf8\")\n            password = raw_password.encode(\"iso8859-1\").decode(\"utf8\")\n            ip = urllib.parse.unquote(headers[\"Client-Ip\"])\n        except:\n            app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}')\n        else:\n            try:\n                user = models.User.query.get(user_email) if '@' in user_email else None\n                is_valid_user = user is not None\n            except sqlalchemy.exc.StatementError as exc:\n                exc = str(exc).split('\\n', 1)[0]\n                app.logger.warn(f'Invalid user {user_email!r}: {exc}')\n            else:\n                ip = urllib.parse.unquote(headers[\"Client-Ip\"])\n                if check_credentials(user, password, ip, protocol, headers[\"Auth-Port\"]):\n                    server, port = get_server(headers[\"Auth-Protocol\"], True)\n                    return {\n                        \"Auth-Status\": \"OK\",\n                        \"Auth-Server\": server,\n                        \"Auth-User\": user_email,\n                        \"Auth-User-Exists\": is_valid_user,\n                        \"Auth-Port\": port\n                    }\n        status, code = get_status(protocol, \"authentication\")\n        return {\n            \"Auth-Status\": status,\n            \"Auth-Error-Code\": code,\n            \"Auth-User\": user_email,\n            \"Auth-User-Exists\": is_valid_user,\n            \"Auth-Wait\": 0\n        }\n    # Unexpected\n    return {}\n\n\ndef get_status(protocol, status):\n    \"\"\" Return the proper error code depending on the protocol\n    \"\"\"\n    status, codes = STATUSES[status]\n    return status, codes[protocol]\n\ndef extract_host_port(host_and_port, default_port):\n    host, _, port = re.match('^(.*?)(:([0-9]*))?$', host_and_port).groups()\n    return host, int(port) if port else default_port\n\ndef get_server(protocol, authenticated=False):\n    if protocol == \"imap\":\n        hostname, port = extract_host_port(app.config['IMAP_ADDRESS'], 143)\n    elif protocol == \"pop3\":\n        hostname, port = extract_host_port(app.config['POP3_ADDRESS'], 110)\n    elif protocol == \"smtp\":\n        if authenticated:\n            hostname, port = extract_host_port(app.config['AUTHSMTP_ADDRESS'], 10025)\n        else:\n            hostname, port = extract_host_port(app.config['SMTP_ADDRESS'], 25)\n    try:\n        # test if hostname is already resolved to an ip adddress\n        ipaddress.ip_address(hostname)\n    except:\n        # hostname is not an ip address - so we need to resolve it\n        hostname = resolve_hostname(hostname)\n    return hostname, port\n\n@tenacity.retry(stop=tenacity.stop_after_attempt(100),\n                wait=tenacity.wait_random(min=2, max=5))\ndef resolve_hostname(hostname):\n    \"\"\" This function uses system DNS to resolve a hostname.\n    It is capable of retrying in case the host is not immediately available\n    \"\"\"\n    return socket.gethostbyname(hostname)\n", "path": "core/admin/mailu/internal/nginx.py"}, {"content": "from mailu import models, utils\nfrom mailu.internal import internal, nginx\nfrom flask import current_app as app\n\nimport flask\nimport flask_login\nimport base64\n\n@internal.route(\"/auth/email\")\ndef nginx_authentication():\n    \"\"\" Main authentication endpoint for Nginx email server\n    \"\"\"\n    client_ip = flask.request.headers[\"Client-Ip\"]\n    headers = flask.request.headers\n    if headers[\"Auth-Port\"] == '25' and headers['Auth-Method'] != 'none':\n        response = flask.Response()\n        response.headers['Auth-Status'] = 'AUTH not supported'\n        response.headers['Auth-Error-Code'] = '502 5.5.1'\n        utils.limiter.rate_limit_ip(client_ip)\n        return response\n    is_from_webmail = headers['Auth-Port'] in ['10143', '10025']\n    if not is_from_webmail and utils.limiter.should_rate_limit_ip(client_ip):\n        status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')\n        response = flask.Response()\n        response.headers['Auth-Status'] = status\n        response.headers['Auth-Error-Code'] = code\n        if int(flask.request.headers['Auth-Login-Attempt']) < 10:\n            response.headers['Auth-Wait'] = '3'\n        return response\n    headers = nginx.handle_authentication(flask.request.headers)\n    response = flask.Response()\n    for key, value in headers.items():\n        response.headers[key] = str(value)\n    is_valid_user = False\n    if response.headers.get(\"Auth-User-Exists\"):\n        username = response.headers[\"Auth-User\"]\n        if utils.limiter.should_rate_limit_user(username, client_ip):\n            # FIXME could be done before handle_authentication()\n            status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')\n            response = flask.Response()\n            response.headers['Auth-Status'] = status\n            response.headers['Auth-Error-Code'] = code\n            if int(flask.request.headers['Auth-Login-Attempt']) < 10:\n                response.headers['Auth-Wait'] = '3'\n            return response\n        is_valid_user = True\n    if headers.get(\"Auth-Status\") == \"OK\":\n        utils.limiter.exempt_ip_from_ratelimits(client_ip)\n    elif is_valid_user:\n        utils.limiter.rate_limit_user(username, client_ip)\n    elif not is_from_webmail:\n        utils.limiter.rate_limit_ip(client_ip)\n    return response\n\n@internal.route(\"/auth/admin\")\ndef admin_authentication():\n    \"\"\" Fails if the user is not an authenticated admin.\n    \"\"\"\n    if (not flask_login.current_user.is_anonymous\n        and flask_login.current_user.global_admin\n        and flask_login.current_user.enabled):\n        return \"\"\n    return flask.abort(403)\n\n@internal.route(\"/auth/user\")\ndef user_authentication():\n    \"\"\" Fails if the user is not authenticated.\n    \"\"\"\n    if (not flask_login.current_user.is_anonymous\n        and flask_login.current_user.enabled):\n        response = flask.Response()\n        email = flask_login.current_user.get_id()\n        response.headers[\"X-User\"] = models.IdnaEmail.process_bind_param(flask_login, email, \"\")\n        response.headers[\"X-User-Token\"] = utils.gen_temp_token(email, flask.session)\n        return response\n    return flask.abort(403)\n\n\n@internal.route(\"/auth/basic\")\ndef basic_authentication():\n    \"\"\" Tries to authenticate using the Authorization header.\n    \"\"\"\n    client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)\n    if utils.limiter.should_rate_limit_ip(client_ip):\n        response = flask.Response(status=401)\n        response.headers[\"WWW-Authenticate\"] = 'Basic realm=\"Authentication rate limit from one source exceeded\"'\n        response.headers['Retry-After'] = '60'\n        return response\n    authorization = flask.request.headers.get(\"Authorization\")\n    if authorization and authorization.startswith(\"Basic \"):\n        encoded = authorization.replace(\"Basic \", \"\")\n        user_email, password = base64.b64decode(encoded).split(b\":\", 1)\n        user_email = user_email.decode(\"utf8\")\n        if utils.limiter.should_rate_limit_user(user_email, client_ip):\n            response = flask.Response(status=401)\n            response.headers[\"WWW-Authenticate\"] = 'Basic realm=\"Authentication rate limit for this username exceeded\"'\n            response.headers['Retry-After'] = '60'\n            return response\n        user = models.User.query.get(user_email)\n        if user and nginx.check_credentials(user, password.decode('utf-8'), client_ip, \"web\"):\n            response = flask.Response()\n            response.headers[\"X-User\"] = models.IdnaEmail.process_bind_param(flask_login, user.email, \"\")\n            utils.limiter.exempt_ip_from_ratelimits(client_ip)\n            return response\n        utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)\n    response = flask.Response(status=401)\n    response.headers[\"WWW-Authenticate\"] = 'Basic realm=\"Login Required\"'\n    return response\n", "path": "core/admin/mailu/internal/views/auth.py"}], "after_files": [{"content": "from mailu import models, utils\nfrom flask import current_app as app\n\nimport re\nimport urllib\nimport ipaddress\nimport socket\nimport sqlalchemy.exc\nimport tenacity\n\nSUPPORTED_AUTH_METHODS = [\"none\", \"plain\"]\n\n\nSTATUSES = {\n    \"authentication\": (\"Authentication credentials invalid\", {\n        \"imap\": \"AUTHENTICATIONFAILED\",\n        \"smtp\": \"535 5.7.8\",\n        \"pop3\": \"-ERR Authentication failed\"\n    }),\n    \"encryption\": (\"Must issue a STARTTLS command first\", {\n        \"smtp\": \"530 5.7.0\"\n    }),\n    \"ratelimit\": (\"Temporary authentication failure (rate-limit)\", {\n        \"imap\": \"LIMIT\",\n        \"smtp\": \"451 4.3.2\",\n        \"pop3\": \"-ERR [LOGIN-DELAY] Retry later\"\n    }),\n}\n\ndef check_credentials(user, password, ip, protocol=None, auth_port=None):\n    if not user or not user.enabled or (protocol == \"imap\" and not user.enable_imap) or (protocol == \"pop3\" and not user.enable_pop):\n        return False\n    is_ok = False\n    # webmails\n    if auth_port in ['10143', '10025'] and password.startswith('token-'):\n        if utils.verify_temp_token(user.get_id(), password):\n            is_ok = True\n    # All tokens are 32 characters hex lowercase\n    if not is_ok and len(password) == 32:\n        for token in user.tokens:\n            if (token.check_password(password) and\n                (not token.ip or token.ip == ip)):\n                    is_ok = True\n                    break\n    if not is_ok and user.check_password(password):\n        is_ok = True\n    return is_ok\n\ndef handle_authentication(headers):\n    \"\"\" Handle an HTTP nginx authentication request\n    See: http://nginx.org/en/docs/mail/ngx_mail_auth_http_module.html#protocol\n    \"\"\"\n    method = headers[\"Auth-Method\"]\n    protocol = headers[\"Auth-Protocol\"]\n    # Incoming mail, no authentication\n    if method == \"none\" and protocol == \"smtp\":\n        server, port = get_server(protocol, False)\n        if app.config[\"INBOUND_TLS_ENFORCE\"]:\n            if \"Auth-SSL\" in headers and headers[\"Auth-SSL\"] == \"on\":\n                return {\n                    \"Auth-Status\": \"OK\",\n                    \"Auth-Server\": server,\n                    \"Auth-Port\": port\n                }\n            else:\n                status, code = get_status(protocol, \"encryption\")\n                return {\n                    \"Auth-Status\": status,\n                    \"Auth-Error-Code\" : code,\n                    \"Auth-Wait\": 0\n                }\n        else:\n            return {\n                \"Auth-Status\": \"OK\",\n                \"Auth-Server\": server,\n                \"Auth-Port\": port\n            }\n    # Authenticated user\n    elif method == \"plain\":\n        is_valid_user = False\n        # According to RFC2616 section 3.7.1 and PEP 3333, HTTP headers should\n        # be ASCII and are generally considered ISO8859-1. However when passing\n        # the password, nginx does not transcode the input UTF string, thus\n        # we need to manually decode.\n        raw_user_email = urllib.parse.unquote(headers[\"Auth-User\"])\n        raw_password = urllib.parse.unquote(headers[\"Auth-Pass\"])\n        user_email = 'invalid'\n        try:\n            user_email = raw_user_email.encode(\"iso8859-1\").decode(\"utf8\")\n            password = raw_password.encode(\"iso8859-1\").decode(\"utf8\")\n            ip = urllib.parse.unquote(headers[\"Client-Ip\"])\n        except:\n            app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}')\n        else:\n            try:\n                user = models.User.query.get(user_email) if '@' in user_email else None\n            except sqlalchemy.exc.StatementError as exc:\n                exc = str(exc).split('\\n', 1)[0]\n                app.logger.warn(f'Invalid user {user_email!r}: {exc}')\n            else:\n                is_valid_user = user is not None\n                ip = urllib.parse.unquote(headers[\"Client-Ip\"])\n                if check_credentials(user, password, ip, protocol, headers[\"Auth-Port\"]):\n                    server, port = get_server(headers[\"Auth-Protocol\"], True)\n                    return {\n                        \"Auth-Status\": \"OK\",\n                        \"Auth-Server\": server,\n                        \"Auth-User\": user_email,\n                        \"Auth-User-Exists\": is_valid_user,\n                        \"Auth-Port\": port\n                    }\n        status, code = get_status(protocol, \"authentication\")\n        return {\n            \"Auth-Status\": status,\n            \"Auth-Error-Code\": code,\n            \"Auth-User\": user_email,\n            \"Auth-User-Exists\": is_valid_user,\n            \"Auth-Wait\": 0\n        }\n    # Unexpected\n    return {}\n\n\ndef get_status(protocol, status):\n    \"\"\" Return the proper error code depending on the protocol\n    \"\"\"\n    status, codes = STATUSES[status]\n    return status, codes[protocol]\n\ndef extract_host_port(host_and_port, default_port):\n    host, _, port = re.match('^(.*?)(:([0-9]*))?$', host_and_port).groups()\n    return host, int(port) if port else default_port\n\ndef get_server(protocol, authenticated=False):\n    if protocol == \"imap\":\n        hostname, port = extract_host_port(app.config['IMAP_ADDRESS'], 143)\n    elif protocol == \"pop3\":\n        hostname, port = extract_host_port(app.config['POP3_ADDRESS'], 110)\n    elif protocol == \"smtp\":\n        if authenticated:\n            hostname, port = extract_host_port(app.config['AUTHSMTP_ADDRESS'], 10025)\n        else:\n            hostname, port = extract_host_port(app.config['SMTP_ADDRESS'], 25)\n    try:\n        # test if hostname is already resolved to an ip adddress\n        ipaddress.ip_address(hostname)\n    except:\n        # hostname is not an ip address - so we need to resolve it\n        hostname = resolve_hostname(hostname)\n    return hostname, port\n\n@tenacity.retry(stop=tenacity.stop_after_attempt(100),\n                wait=tenacity.wait_random(min=2, max=5))\ndef resolve_hostname(hostname):\n    \"\"\" This function uses system DNS to resolve a hostname.\n    It is capable of retrying in case the host is not immediately available\n    \"\"\"\n    return socket.gethostbyname(hostname)\n", "path": "core/admin/mailu/internal/nginx.py"}, {"content": "from mailu import models, utils\nfrom mailu.internal import internal, nginx\nfrom flask import current_app as app\n\nimport flask\nimport flask_login\nimport base64\n\n@internal.route(\"/auth/email\")\ndef nginx_authentication():\n    \"\"\" Main authentication endpoint for Nginx email server\n    \"\"\"\n    client_ip = flask.request.headers[\"Client-Ip\"]\n    headers = flask.request.headers\n    if headers[\"Auth-Port\"] == '25' and headers['Auth-Method'] != 'none':\n        response = flask.Response()\n        response.headers['Auth-Status'] = 'AUTH not supported'\n        response.headers['Auth-Error-Code'] = '502 5.5.1'\n        utils.limiter.rate_limit_ip(client_ip)\n        return response\n    is_from_webmail = headers['Auth-Port'] in ['10143', '10025']\n    if not is_from_webmail and utils.limiter.should_rate_limit_ip(client_ip):\n        status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')\n        response = flask.Response()\n        response.headers['Auth-Status'] = status\n        response.headers['Auth-Error-Code'] = code\n        if int(flask.request.headers['Auth-Login-Attempt']) < 10:\n            response.headers['Auth-Wait'] = '3'\n        return response\n    headers = nginx.handle_authentication(flask.request.headers)\n    response = flask.Response()\n    for key, value in headers.items():\n        response.headers[key] = str(value)\n    is_valid_user = False\n    if response.headers.get(\"Auth-User-Exists\") == \"True\":\n        username = response.headers[\"Auth-User\"]\n        if utils.limiter.should_rate_limit_user(username, client_ip):\n            # FIXME could be done before handle_authentication()\n            status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit')\n            response = flask.Response()\n            response.headers['Auth-Status'] = status\n            response.headers['Auth-Error-Code'] = code\n            if int(flask.request.headers['Auth-Login-Attempt']) < 10:\n                response.headers['Auth-Wait'] = '3'\n            return response\n        is_valid_user = True\n    if headers.get(\"Auth-Status\") == \"OK\":\n        utils.limiter.exempt_ip_from_ratelimits(client_ip)\n    elif is_valid_user:\n        utils.limiter.rate_limit_user(username, client_ip)\n    elif not is_from_webmail:\n        utils.limiter.rate_limit_ip(client_ip)\n    return response\n\n@internal.route(\"/auth/admin\")\ndef admin_authentication():\n    \"\"\" Fails if the user is not an authenticated admin.\n    \"\"\"\n    if (not flask_login.current_user.is_anonymous\n        and flask_login.current_user.global_admin\n        and flask_login.current_user.enabled):\n        return \"\"\n    return flask.abort(403)\n\n@internal.route(\"/auth/user\")\ndef user_authentication():\n    \"\"\" Fails if the user is not authenticated.\n    \"\"\"\n    if (not flask_login.current_user.is_anonymous\n        and flask_login.current_user.enabled):\n        response = flask.Response()\n        email = flask_login.current_user.get_id()\n        response.headers[\"X-User\"] = models.IdnaEmail.process_bind_param(flask_login, email, \"\")\n        response.headers[\"X-User-Token\"] = utils.gen_temp_token(email, flask.session)\n        return response\n    return flask.abort(403)\n\n\n@internal.route(\"/auth/basic\")\ndef basic_authentication():\n    \"\"\" Tries to authenticate using the Authorization header.\n    \"\"\"\n    client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr)\n    if utils.limiter.should_rate_limit_ip(client_ip):\n        response = flask.Response(status=401)\n        response.headers[\"WWW-Authenticate\"] = 'Basic realm=\"Authentication rate limit from one source exceeded\"'\n        response.headers['Retry-After'] = '60'\n        return response\n    authorization = flask.request.headers.get(\"Authorization\")\n    if authorization and authorization.startswith(\"Basic \"):\n        encoded = authorization.replace(\"Basic \", \"\")\n        user_email, password = base64.b64decode(encoded).split(b\":\", 1)\n        user_email = user_email.decode(\"utf8\")\n        if utils.limiter.should_rate_limit_user(user_email, client_ip):\n            response = flask.Response(status=401)\n            response.headers[\"WWW-Authenticate\"] = 'Basic realm=\"Authentication rate limit for this username exceeded\"'\n            response.headers['Retry-After'] = '60'\n            return response\n        user = models.User.query.get(user_email)\n        if user and nginx.check_credentials(user, password.decode('utf-8'), client_ip, \"web\"):\n            response = flask.Response()\n            response.headers[\"X-User\"] = models.IdnaEmail.process_bind_param(flask_login, user.email, \"\")\n            utils.limiter.exempt_ip_from_ratelimits(client_ip)\n            return response\n        utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip)\n    response = flask.Response(status=401)\n    response.headers[\"WWW-Authenticate\"] = 'Basic realm=\"Login Required\"'\n    return response\n", "path": "core/admin/mailu/internal/views/auth.py"}]}