Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions Doc/library/imaplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -430,13 +430,30 @@ An :class:`IMAP4` instance has the following methods:
.. method:: IMAP4.login_cram_md5(user, password)

Force use of ``CRAM-MD5`` authentication when identifying the client to protect
the password. Will only work if the server ``CAPABILITY`` response includes the
phrase ``AUTH=CRAM-MD5``.
the password. It will only work if the server ``CAPABILITY`` response includes
the phrase ``AUTH=CRAM-MD5``.

.. versionchanged:: 3.15
An :exc:`IMAP4.error` is raised if MD5 support is not available.


.. method:: IMAP4.login_plain(user, password)

Authenticate using the ``PLAIN`` SASL mechanism (:rfc:`4616`).

This is a plaintext authentication mechanism that can be used instead
of :meth:`login` when UTF-8 support is required (see :rfc:`6855`).
Since the credentials are only base64-encoded, not encrypted, this
method should only be used over a TLS-protected connection, such as
:class:`IMAP4_SSL` or after :meth:`starttls`.

It will only work if the server supports the ``PLAIN`` mechanism,
which it need not advertise as ``AUTH=PLAIN`` in its ``CAPABILITY``
response.

.. versionadded:: next


.. method:: IMAP4.logout()

Shutdown connection to server. Returns server ``BYE`` response.
Expand Down
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ imaplib
a wrapper for the ``MOVE`` command (:rfc:`6851`).
(Contributed by Serhiy Storchaka in :gh:`77508`.)

* Add the :meth:`~imaplib.IMAP4.login_plain` method, which authenticates
using the ``PLAIN`` SASL mechanism (:rfc:`4616`) and supports non-ASCII
user names and passwords.
(Contributed by Przemysław Buczkowski and Serhiy Storchaka in :gh:`89869`.)


logging
-------
Expand Down
24 changes: 24 additions & 0 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,30 @@ def login(self, user, password):
return typ, dat


def login_plain(self, user, password):
"""Authenticate using the PLAIN SASL mechanism (RFC 4616).

This is a plaintext authentication mechanism that can be used
instead of login() when UTF-8 support is required. Since the
credentials are only base64-encoded, not encrypted, it should
only be used over a TLS-protected connection.

'user' and 'password' can be strings (encoded to UTF-8) or
bytes-like objects.
"""
if isinstance(user, str):
user = user.encode('utf-8')
if isinstance(password, str):
password = password.encode('utf-8')
if b'\0' in user or b'\0' in password:
raise ValueError("NUL is not allowed in user name or password")
# An empty authorization identity (RFC 4616) makes the server
# derive it from the authentication identity; a non-empty one
# requests proxy authorization and is often rejected.
response = b'\0' + bytes(user) + b'\0' + bytes(password)
return self.authenticate("PLAIN", lambda _: response)


def login_cram_md5(self, user, password):
""" Force use of CRAM-MD5 authentication.

Expand Down
38 changes: 38 additions & 0 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,15 @@ def cmd_AUTHENTICATE(self, tag, args):
self._send_tagged(tag, 'NO', 'No access')


class AuthHandler_PLAIN(SimpleIMAPHandler):
capabilities = 'LOGINDISABLED AUTH=PLAIN'
def cmd_AUTHENTICATE(self, tag, args):
self.server.auth_args = args
self._send_textline('+')
self.server.response = yield
self._send_tagged(tag, 'OK', 'Logged in.')


class NewIMAPTestsMixin:
client = None

Expand Down Expand Up @@ -692,6 +701,35 @@ def test_login_cram_md5_blocked(self):
with self.assertRaisesRegex(imaplib.IMAP4.error, msg):
client.login_cram_md5("tim", b"tanstaaftanstaaf")

def test_login_plain_ascii(self):
client, server = self._setup(AuthHandler_PLAIN)
self.assertIn('AUTH=PLAIN', client.capabilities)
ret, _ = client.login_plain("prem", "pass")
self.assertEqual(ret, "OK")
self.assertEqual(server.auth_args, ['PLAIN'])
self.assertEqual(server.response, b'AHByZW0AcGFzcw==\r\n')

def test_login_plain_utf8(self):
client, server = self._setup(AuthHandler_PLAIN)
self.assertIn('AUTH=PLAIN', client.capabilities)
ret, _ = client.login_plain("pręm", "żółć")
self.assertEqual(ret, "OK")
self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')

def test_login_plain_bytes(self):
# bytes are accepted and sent verbatim (not re-encoded).
client, server = self._setup(AuthHandler_PLAIN)
ret, _ = client.login_plain(b'pr\xc4\x99m', b'\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87')
self.assertEqual(ret, "OK")
self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')

def test_login_plain_nul(self):
client, _ = self._setup(AuthHandler_PLAIN)
with self.assertRaises(ValueError):
client.login_plain('user\0', 'pass')
with self.assertRaises(ValueError):
client.login_plain('user', b'pa\0ss')

def test_aborted_authentication(self):
class MyServer(SimpleIMAPHandler):
def cmd_AUTHENTICATE(self, tag, args):
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ Stan Bubrouski
Brandt Bucher
Curtis Bucher
Colm Buckley
Przemysław Buczkowski
Erik de Bueger
Jan-Hein Bührman
Marc Bürg
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :meth:`imaplib.IMAP4.login_plain`, which authenticates using the
``PLAIN`` SASL mechanism (:rfc:`4616`). Unlike :meth:`~imaplib.IMAP4.login`,
it supports non-ASCII user names and passwords.
Loading