Skip to content

Commit

Permalink
Merge pull request #1681 from pbiering/merge-auth-imap
Browse files Browse the repository at this point in the history
Merge auth imap
  • Loading branch information
pbiering authored Jan 16, 2025
2 parents a93af6f + 3df5d28 commit f9457f0
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,10 @@ 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.

Default: `none`

Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions config
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 9 additions & 1 deletion radicale/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")


Expand Down Expand Up @@ -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)
Expand Down
70 changes: 70 additions & 0 deletions radicale/auth/imap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# RadicaleIMAP IMAP authentication plugin for Radicale.
# Copyright © 2017, 2020 Unrud <[email protected]>
# Copyright © 2025-2025 Peter Bieringer <[email protected]>
#
# 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 <http://www.gnu.org/licenses/>.

import imaplib
import ssl

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:
connection: imaplib.IMAP4 | imaplib.IMAP4_SSL
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 ""
31 changes: 31 additions & 0 deletions radicale/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit f9457f0

Please sign in to comment.