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
34 changes: 32 additions & 2 deletions Doc/library/imaplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,19 @@ An :class:`IMAP4` instance has the following methods:
of the IMAP4 QUOTA extension defined in rfc2087.


.. method:: IMAP4.id(fields=None)

Send client identification information to the server
and return the identification information sent back by the server
(the ``ID`` command, defined in :rfc:`2971`).
*fields* is a mapping of field names to values
(for example, ``{'name': 'myclient', 'version': '1.0'}``);
a value can be ``None``.
The server must support the ``ID`` capability.

.. versionadded:: next


.. method:: IMAP4.idle(duration=None)

Return an :class:`!Idler`: an iterable context manager implementing the
Expand Down Expand Up @@ -430,13 +443,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
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,19 @@ io
imaplib
-------

* Add the :meth:`~imaplib.IMAP4.id` method,
a wrapper for the ``ID`` command (:rfc:`2971`).
(Contributed by Serhiy Storchaka in :gh:`98092`.)

* Add the :meth:`~imaplib.IMAP4.move` method,
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
47 changes: 47 additions & 0 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
'GETANNOTATION':('AUTH', 'SELECTED'),
'GETQUOTA': ('AUTH', 'SELECTED'),
'GETQUOTAROOT': ('AUTH', 'SELECTED'),
'ID': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
'IDLE': ('AUTH', 'SELECTED'),
'MYRIGHTS': ('AUTH', 'SELECTED'),
'LIST': ('AUTH', 'SELECTED'),
Expand Down Expand Up @@ -697,6 +698,28 @@ def getquotaroot(self, mailbox):
return typ, [quotaroot, quota]


def id(self, fields=None):
"""Send client identification information to the server.

(typ, [data]) = <instance>.id(fields)

'fields' is a mapping of field names to values; a value can be
None. 'data' is the identification information sent back by
the server, in the same parenthesized list form.
"""
name = 'ID'
if fields:
items = []
for field, value in fields.items():
items.append(self._quote(field))
items.append(b'NIL' if value is None else self._quote(value))
arg = b'(' + b' '.join(items) + b')'
else:
arg = 'NIL'
typ, dat = self._simple_command(name, arg)
return self._untagged_response(typ, dat, name)


def idle(self, duration=None):
"""Return an iterable IDLE context manager producing untagged responses.
If the argument is not None, limit iteration to 'duration' seconds.
Expand Down Expand Up @@ -739,6 +762,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
4 changes: 2 additions & 2 deletions Lib/multiprocessing/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1294,9 +1294,9 @@ class SyncManager(BaseManager):
class _SharedMemoryTracker:
"Manages one or more shared memory segments."

def __init__(self, name, segment_names=[]):
def __init__(self, name, segment_names=None):
self.shared_memory_context_name = name
self.segment_names = segment_names
self.segment_names = [] if segment_names is None else segment_names

def register_segment(self, segment_name):
"Adds the supplied shared memory block name to tracker."
Expand Down
56 changes: 56 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 Expand Up @@ -1701,6 +1739,24 @@ def test_getquotaroot(self):
self.assertEqual(typ, 'OK')
self.assertEqual(server.args, ['"New folder"'])

def test_id(self):
client, server = self._setup(make_simple_handler('ID',
['* ID ("name" "Cyrus" "version" "1.5")']))
typ, data = client.id({'name': 'imaplib', 'version': '3.16'})
self.assertEqual(typ, 'OK')
self.assertEqual(data, [b'("name" "Cyrus" "version" "1.5")'])
self.assertEqual(server.args, ['("name" "imaplib" "version" "3.16")'])

typ, data = client.id()
self.assertEqual(typ, 'OK')
self.assertEqual(server.args, ['NIL'])

# Fields and values are quoted strings; a None value is sent
# as NIL.
typ, data = client.id({'name': 'my "client"', 'os': None})
self.assertEqual(typ, 'OK')
self.assertEqual(server.args, [r'("name" "my \"client\"" "os" NIL)'])

def test_setquota(self):
client, server = self._setup(make_simple_handler('SETQUOTA',
['* QUOTA "" (STORAGE 512)']))
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,2 @@
Add :meth:`imaplib.IMAP4.id`, a wrapper for the IMAP ``ID`` command
(:rfc:`2971`).
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