diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 65681ec093598c..b33621b086d283 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -330,8 +330,22 @@ 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``. + + +.. method:: IMAP4.login_plain(login, password) + + Authenticate using PLAIN SASL mechanism. + + This is a plain-text authentication mechanism that can be used + instead of :meth:`IMAP4.login()` when UTF-8 support is required. + See :RFC:`6855`, page 5. + + It will only work if the server ``CAPABILITY`` response includes + the phrase ``AUTH=PLAIN``. + + .. versionadded:: 3.11 .. method:: IMAP4.logout() diff --git a/Lib/imaplib.py b/Lib/imaplib.py index fa4c0f8f62361a..e9d90f831dd3b7 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -614,6 +614,18 @@ def login(self, user, password): return typ, dat + def login_plain(self, user, password): + """Authenticate using PLAIN SASL mechanism. + + This is a plain-text authentication mechanism that can be used + instead of login() when UTF-8 support is required. + """ + return self.authenticate( + "PLAIN", + lambda _: "{0}\x00{0}\x00{1}".format(user, password).encode() + ) + + def login_cram_md5(self, user, password): """ Force use of CRAM-MD5 authentication. diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index c2b935f58164e5..da4b74c8e33cf8 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -423,6 +423,36 @@ def cmd_AUTHENTICATE(self, tag, args): ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf") self.assertEqual(ret, "OK") + def test_login_plain_ascii(self): + class AuthHandler(SimpleIMAPHandler): + capabilities = 'LOGINDISABLED AUTH=PLAIN' + def cmd_AUTHENTICATE(self, tag, args): + self._send_textline('+') + r = yield + if r == b'cHJlbQBwcmVtAHBhc3M=\r\n': + self._send_tagged(tag, 'OK', 'Logged in.') + else: + self._send_tagged(tag, 'NO', 'No access') + client, _ = self._setup(AuthHandler) + self.assertTrue('AUTH=PLAIN' in client.capabilities) + ret, _ = client.login_plain("prem", "pass") + self.assertEqual(ret, "OK") + + def test_login_plain_utf8(self): + class AuthHandler(SimpleIMAPHandler): + capabilities = 'LOGINDISABLED AUTH=PLAIN' + def cmd_AUTHENTICATE(self, tag, args): + self._send_textline('+') + r = yield + if r == b'cHLEmW0AcHLEmW0AxbzDs8WCxIc=\r\n': + self._send_tagged(tag, 'OK', 'Logged in.') + else: + self._send_tagged(tag, 'NO', 'No access') + client, _ = self._setup(AuthHandler) + self.assertTrue('AUTH=PLAIN' in client.capabilities) + ret, _ = client.login_plain("pręm", "żółć") + self.assertEqual(ret, "OK") + def test_aborted_authentication(self): class MyServer(SimpleIMAPHandler): def cmd_AUTHENTICATE(self, tag, args): diff --git a/Misc/ACKS b/Misc/ACKS index 204293fa50d9c0..34c969fe6824be 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -248,6 +248,7 @@ Stan Bubrouski Brandt Bucher Curtis Bucher Colm Buckley +Przemysław Buczkowski Erik de Bueger Jan-Hein Bührman Lars Buitinck diff --git a/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst b/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst new file mode 100644 index 00000000000000..7b2e7bffe531a7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst @@ -0,0 +1 @@ +Add :meth:`imaplib.IMAP4.login_plain`.