From 3e18644423430cb81fecc9601c7f8130c95fda00 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 16 Jan 2025 05:59:52 +0100 Subject: [PATCH 1/7] imap: changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b8cbc8..cb7952a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 3.4.1.dev * Add: option [auth] dovecot_connection_type / dovecot_host / dovecot_port +* Add: option [auth] type imap by code migration from https://github.com/Unrud/RadicaleIMAP/ ## 3.4.0 * Add: option [auth] cache_logins/cache_successful_logins_expiry/cache_failed_logins for caching logins From c24659c5ec01708d634dcf1cc6d32d8e69aa1a17 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 16 Jan 2025 06:01:01 +0100 Subject: [PATCH 2/7] imap: doc and default config --- DOCUMENTATION.md | 15 +++++++++++++++ config | 8 ++++++++ 2 files changed, 23 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index a68cc2c4..ddfaa547 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -812,6 +812,9 @@ Available backends: `dovecot` : Use a local Dovecot server to authenticate users. +`imap` +: Use a IMAP server to authenticate users. + Default: `none` ##### cache_logins @@ -993,6 +996,18 @@ Port of via network exposed dovecot socket Default: `12345` +##### imap_host + +IMAP server hostname: address | address:port | [address]:port | imap.server.tld + +Default: `localhost` + +##### imap_security + +Secure the IMAP connection: tls | starttls | none + +Default: `tls` + ##### lc_username Сonvert username to lowercase, must be true for case-insensitive auth diff --git a/config b/config index a0f6cfa7..c775a3c1 100644 --- a/config +++ b/config @@ -117,6 +117,14 @@ # Port of via network exposed dovecot socket #dovecot_port = 12345 +# IMAP server hostname +# Syntax: address | address:port | [address]:port | imap.server.tld +#imap_host = localhost + +# Secure the IMAP connection +# Value: tls | starttls | none +#imap_security = tls + # Htpasswd filename #htpasswd_filename = /etc/radicale/users From 72c7d32e44060ca70e1903b8cadeab29d3a1c8c9 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 16 Jan 2025 06:01:29 +0100 Subject: [PATCH 3/7] dovecot: extend doc --- DOCUMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index ddfaa547..30566c33 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -810,7 +810,7 @@ Available backends: : Use a LDAP or AD server to authenticate users. `dovecot` -: Use a local Dovecot server to authenticate users. +: Use a Dovecot server to authenticate users. `imap` : Use a IMAP server to authenticate users. From 50b76f71143d9b79a0b12106d9016bcc52b7cc17 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 16 Jan 2025 06:02:06 +0100 Subject: [PATCH 4/7] imap: config parse --- radicale/auth/__init__.py | 10 +++++++++- radicale/config.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/radicale/auth/__init__.py b/radicale/auth/__init__.py index 8bc8ffad..71854e2a 100644 --- a/radicale/auth/__init__.py +++ b/radicale/auth/__init__.py @@ -41,8 +41,16 @@ "denyall", "htpasswd", "ldap", + "imap", "dovecot") +CACHE_LOGIN_TYPES: Sequence[str] = ( + "dovecot", + "ldap", + "htpasswd", + "imap", + ) + AUTH_SOCKET_FAMILY: Sequence[str] = ("AF_UNIX", "AF_INET", "AF_INET6") @@ -97,7 +105,7 @@ def __init__(self, configuration: "config.Configuration") -> None: # cache_successful_logins self._cache_logins = configuration.get("auth", "cache_logins") self._type = configuration.get("auth", "type") - if (self._type in ["dovecot", "ldap", "htpasswd"]) or (self._cache_logins is False): + if (self._type in CACHE_LOGIN_TYPES) or (self._cache_logins is False): logger.info("auth.cache_logins: %s", self._cache_logins) else: logger.info("auth.cache_logins: %s (but not required for type '%s' and disabled therefore)", self._cache_logins, self._type) diff --git a/radicale/config.py b/radicale/config.py index 86970732..9b4e9af4 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -104,6 +104,29 @@ def _convert_to_bool(value: Any) -> bool: return RawConfigParser.BOOLEAN_STATES[value.lower()] +def imap_address(value): + if "]" in value: + pre_address, pre_address_port = value.rsplit("]", 1) + else: + pre_address, pre_address_port = "", value + if ":" in pre_address_port: + pre_address2, port = pre_address_port.rsplit(":", 1) + address = pre_address + pre_address2 + else: + address, port = pre_address + pre_address_port, None + try: + return (address.strip(string.whitespace + "[]"), + None if port is None else int(port)) + except ValueError: + raise ValueError("malformed IMAP address: %r" % value) + + +def imap_security(value): + if value not in ("tls", "starttls", "none"): + raise ValueError("unsupported IMAP security: %r" % value) + return value + + def json_str(value: Any) -> dict: if not value: return {} @@ -276,6 +299,14 @@ def json_str(value: Any) -> dict: "value": "", "help": "The path to the CA file in pem format which is used to certificate the server certificate", "type": str}), + ("imap_host", { + "value": "localhost", + "help": "IMAP server hostname: address|address:port|[address]:port|*localhost*", + "type": imap_address}), + ("imap_security", { + "value": "tls", + "help": "Secure the IMAP connection: *tls*|starttls|none", + "type": imap_security}), ("strip_domain", { "value": "False", "help": "strip domain from username", From bc939522dc3ad3b56026d23234ac06a2008e5847 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 16 Jan 2025 06:02:22 +0100 Subject: [PATCH 5/7] imap: migrate from https://github.com/Unrud/RadicaleIMAP/ --- radicale/auth/imap.py | 71 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 radicale/auth/imap.py diff --git a/radicale/auth/imap.py b/radicale/auth/imap.py new file mode 100644 index 00000000..66b67935 --- /dev/null +++ b/radicale/auth/imap.py @@ -0,0 +1,71 @@ +# RadicaleIMAP IMAP authentication plugin for Radicale. +# Copyright © 2017, 2020 Unrud +# Copyright © 2025-2025 Peter Bieringer +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import imaplib +import ssl +import string + +from radicale import auth +from radicale.log import logger + + +class Auth(auth.BaseAuth): + """Authenticate user with IMAP.""" + + def __init__(self, configuration) -> None: + super().__init__(configuration) + self._host, self._port = self.configuration.get("auth", "imap_host") + logger.info("auth imap host: %r", self._host) + self._security = self.configuration.get("auth", "imap_security") + if self._security == "none": + logger.info("auth imap security: %s (INSECURE, credentials are transmitted in clear text)", self._security) + else: + logger.info("auth imap security: %s", self._security) + if self._security == "tls": + if self._port is None: + self._port = 993 + logger.info("auth imap port (autoselected): %d", self._port) + else: + logger.info("auth imap port: %d", self._port) + else: + if self._port is None: + self._port = 143 + logger.info("auth imap port (autoselected): %d", self._port) + else: + logger.info("auth imap port: %d", self._port) + + def _login(self, login, password) -> str: + try: + if self._security == "tls": + connection = imaplib.IMAP4_SSL( + host=self._host, port=self._port, + ssl_context=ssl.create_default_context()) + else: + connection = imaplib.IMAP4(host=self._host, port=self._port) + if self._security == "starttls": + connection.starttls(ssl.create_default_context()) + try: + connection.login(login, password) + except imaplib.IMAP4.error as e: + logger.warning("IMAP authentication failed for user %r: %s", login, e, exc_info=False) + return "" + connection.logout() + return login + except (OSError, imaplib.IMAP4.error) as e: + logger.error("Failed to communicate with IMAP server %r: " + "%s" % ("[%s]:%d" % (self._host, self._port), e)) + return "" From e80bf589012a8fcb4864c439f709fe8d97682521 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 16 Jan 2025 06:05:14 +0100 Subject: [PATCH 6/7] imap: flake8 fixes --- radicale/auth/imap.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/radicale/auth/imap.py b/radicale/auth/imap.py index 66b67935..0f78bca9 100644 --- a/radicale/auth/imap.py +++ b/radicale/auth/imap.py @@ -17,7 +17,6 @@ import imaplib import ssl -import string from radicale import auth from radicale.log import logger @@ -66,6 +65,5 @@ def _login(self, login, password) -> str: connection.logout() return login except (OSError, imaplib.IMAP4.error) as e: - logger.error("Failed to communicate with IMAP server %r: " - "%s" % ("[%s]:%d" % (self._host, self._port), e)) + logger.error("Failed to communicate with IMAP server %r: %s" % ("[%s]:%d" % (self._host, self._port), e)) return "" From 3df5d28432e76a269d2212949b8fc81e07229a16 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 16 Jan 2025 06:11:57 +0100 Subject: [PATCH 7/7] imap: mypy fix --- radicale/auth/imap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/radicale/auth/imap.py b/radicale/auth/imap.py index 0f78bca9..8b3c2972 100644 --- a/radicale/auth/imap.py +++ b/radicale/auth/imap.py @@ -49,6 +49,7 @@ def __init__(self, configuration) -> None: def _login(self, login, password) -> str: try: + connection: imaplib.IMAP4 | imaplib.IMAP4_SSL if self._security == "tls": connection = imaplib.IMAP4_SSL( host=self._host, port=self._port,