Files
Radicale/radicale/auth/imap.py

96 lines
4.4 KiB
Python

# RadicaleIMAP IMAP authentication plugin for Radicale.
# Copyright © 2017, 2020 Unrud <unrud@outlook.com>
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
#
# 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
import sys
from typing import Union
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.warning("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)
self._append_domain = self.configuration.get("auth", "imap_append_domain")
if self._append_domain is None or len(self._append_domain) == 0:
logger.info("auth imap append domain not used")
else:
logger.info("auth imap append domain: %s", self._append_domain)
def _login(self, login, password) -> str:
try:
if sys.version_info < (3, 10):
connection: Union[imaplib.IMAP4, imaplib.IMAP4_SSL]
else:
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())
if self._append_domain is None or len(self._append_domain) == 0:
imaplogin = login
else:
imaplogin = login + "@" + self._append_domain
try:
if "AUTH=PLAIN" in connection.capabilities:
logger.debug("IMAP authentication PLAIN selected for user %r via %s:%d (security: %s)", imaplogin, self._host, self._port, self._security)
connection.authenticate(
"PLAIN",
lambda _: "{0}\x00{0}\x00{1}".format(imaplogin, password).encode(),
)
elif "AUTH=LOGIN" in connection.capabilities:
logger.debug("IMAP authentication LOGIN selected for user %r via %s:%d (security: %s)", imaplogin, self._host, self._port, self._security)
connection.login(imaplogin, password)
else:
logger.error("IMAP server is neither supporting AUTH=PLAIN or AUTH=LOGIN: %s:%d (security: %s)", self._host, self._port, self._security)
return ""
except imaplib.IMAP4.error as e:
logger.warning("IMAP authentication failed for user %r: %s", imaplogin, 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 ""