From db095b7797c1a1ce326445db9a30c3bdb4049602 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Przemys=C5=82aw=20Buczkowski?= <prem@prem.moe>
Date: Thu, 4 Nov 2021 00:10:11 +0000
Subject: [PATCH 107/107] Add imaplib.IMAP4.login_plain

Adds authentication using PLAIN SASL mechanism.

This is a plain-text authentication mechanism which can be used
instead of :meth:`IMAP4.login()` when UTF-8 support is required.
---
 Doc/library/imaplib.rst                       | 18 +++++++++--
 Lib/imaplib.py                                | 12 ++++++++
 Lib/test/test_imaplib.py                      | 30 +++++++++++++++++++
 Misc/ACKS                                     |  1 +
 .../2021-11-04-00-09-09.bpo-12345.XG7aHz.rst  |  1 +
 5 files changed, 60 insertions(+), 2 deletions(-)
 create mode 100644 Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-12345.XG7aHz.rst

diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst
index 65681ec093..21bf035d86 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 which 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 fa4c0f8f62..30843943e6 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 which 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 c2b935f581..da4b74c8e3 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 204293fa50..34c969fe68 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-12345.XG7aHz.rst b/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-12345.XG7aHz.rst
new file mode 100644
index 0000000000..7b2e7bffe5
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-12345.XG7aHz.rst
@@ -0,0 +1 @@
+Add :meth:`imaplib.IMAP4.login_plain`.
-- 
2.33.1

