Skip to content

Commit a47c55f

Browse files
Merge remote-tracking branch 'upstream/main' into sre-branch-peek
2 parents afe1b03 + b175621 commit a47c55f

16 files changed

Lines changed: 286 additions & 50 deletions

File tree

Doc/library/imaplib.rst

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,19 @@ An :class:`IMAP4` instance has the following methods:
328328
of the IMAP4 QUOTA extension defined in rfc2087.
329329

330330

331+
.. method:: IMAP4.id(fields=None)
332+
333+
Send client identification information to the server
334+
and return the identification information sent back by the server
335+
(the ``ID`` command, defined in :rfc:`2971`).
336+
*fields* is a mapping of field names to values
337+
(for example, ``{'name': 'myclient', 'version': '1.0'}``);
338+
a value can be ``None``.
339+
The server must support the ``ID`` capability.
340+
341+
.. versionadded:: next
342+
343+
331344
.. method:: IMAP4.idle(duration=None)
332345

333346
Return an :class:`!Idler`: an iterable context manager implementing the
@@ -430,13 +443,30 @@ An :class:`IMAP4` instance has the following methods:
430443
.. method:: IMAP4.login_cram_md5(user, password)
431444

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

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

439452

453+
.. method:: IMAP4.login_plain(user, password)
454+
455+
Authenticate using the ``PLAIN`` SASL mechanism (:rfc:`4616`).
456+
457+
This is a plaintext authentication mechanism that can be used instead
458+
of :meth:`login` when UTF-8 support is required (see :rfc:`6855`).
459+
Since the credentials are only base64-encoded, not encrypted, this
460+
method should only be used over a TLS-protected connection, such as
461+
:class:`IMAP4_SSL` or after :meth:`starttls`.
462+
463+
It will only work if the server supports the ``PLAIN`` mechanism,
464+
which it need not advertise as ``AUTH=PLAIN`` in its ``CAPABILITY``
465+
response.
466+
467+
.. versionadded:: next
468+
469+
440470
.. method:: IMAP4.logout()
441471

442472
Shutdown connection to server. Returns server ``BYE`` response.

Doc/library/urllib.parse.rst

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,16 @@ or on combining URL components into a URL string.
6060
*missing_as_none* is true.
6161
Not defined component are represented an empty string (by default) or
6262
``None`` if *missing_as_none* is true.
63-
The components are not broken up
64-
into smaller parts (for example, the network location is a single string), and %
65-
escapes are not expanded. The delimiters as shown above are not part of the
66-
result, except for a leading slash in the *path* component, which is retained if
67-
present. For example:
63+
The delimiters as shown above are not part of the result, except for a
64+
leading slash in the *path* component, which is retained if present.
65+
66+
Additionally, the netloc property is broken down into these additional
67+
attributes added to the returned object: username, password, hostname, and
68+
port.
69+
70+
Percent-encoded sequences are not decoded.
71+
72+
For example:
6873

6974
.. doctest::
7075
:options: +NORMALIZE_WHITESPACE

Doc/whatsnew/3.16.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,19 @@ io
231231
imaplib
232232
-------
233233

234+
* Add the :meth:`~imaplib.IMAP4.id` method,
235+
a wrapper for the ``ID`` command (:rfc:`2971`).
236+
(Contributed by Serhiy Storchaka in :gh:`98092`.)
237+
234238
* Add the :meth:`~imaplib.IMAP4.move` method,
235239
a wrapper for the ``MOVE`` command (:rfc:`6851`).
236240
(Contributed by Serhiy Storchaka in :gh:`77508`.)
237241

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

239248
logging
240249
-------

Lib/imaplib.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
'GETANNOTATION':('AUTH', 'SELECTED'),
7474
'GETQUOTA': ('AUTH', 'SELECTED'),
7575
'GETQUOTAROOT': ('AUTH', 'SELECTED'),
76+
'ID': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
7677
'IDLE': ('AUTH', 'SELECTED'),
7778
'MYRIGHTS': ('AUTH', 'SELECTED'),
7879
'LIST': ('AUTH', 'SELECTED'),
@@ -697,6 +698,28 @@ def getquotaroot(self, mailbox):
697698
return typ, [quotaroot, quota]
698699

699700

701+
def id(self, fields=None):
702+
"""Send client identification information to the server.
703+
704+
(typ, [data]) = <instance>.id(fields)
705+
706+
'fields' is a mapping of field names to values; a value can be
707+
None. 'data' is the identification information sent back by
708+
the server, in the same parenthesized list form.
709+
"""
710+
name = 'ID'
711+
if fields:
712+
items = []
713+
for field, value in fields.items():
714+
items.append(self._quote(field))
715+
items.append(b'NIL' if value is None else self._quote(value))
716+
arg = b'(' + b' '.join(items) + b')'
717+
else:
718+
arg = 'NIL'
719+
typ, dat = self._simple_command(name, arg)
720+
return self._untagged_response(typ, dat, name)
721+
722+
700723
def idle(self, duration=None):
701724
"""Return an iterable IDLE context manager producing untagged responses.
702725
If the argument is not None, limit iteration to 'duration' seconds.
@@ -739,6 +762,30 @@ def login(self, user, password):
739762
return typ, dat
740763

741764

765+
def login_plain(self, user, password):
766+
"""Authenticate using the PLAIN SASL mechanism (RFC 4616).
767+
768+
This is a plaintext authentication mechanism that can be used
769+
instead of login() when UTF-8 support is required. Since the
770+
credentials are only base64-encoded, not encrypted, it should
771+
only be used over a TLS-protected connection.
772+
773+
'user' and 'password' can be strings (encoded to UTF-8) or
774+
bytes-like objects.
775+
"""
776+
if isinstance(user, str):
777+
user = user.encode('utf-8')
778+
if isinstance(password, str):
779+
password = password.encode('utf-8')
780+
if b'\0' in user or b'\0' in password:
781+
raise ValueError("NUL is not allowed in user name or password")
782+
# An empty authorization identity (RFC 4616) makes the server
783+
# derive it from the authentication identity; a non-empty one
784+
# requests proxy authorization and is often rejected.
785+
response = b'\0' + bytes(user) + b'\0' + bytes(password)
786+
return self.authenticate("PLAIN", lambda _: response)
787+
788+
742789
def login_cram_md5(self, user, password):
743790
""" Force use of CRAM-MD5 authentication.
744791

Lib/multiprocessing/managers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,9 +1294,9 @@ class SyncManager(BaseManager):
12941294
class _SharedMemoryTracker:
12951295
"Manages one or more shared memory segments."
12961296

1297-
def __init__(self, name, segment_names=[]):
1297+
def __init__(self, name, segment_names=None):
12981298
self.shared_memory_context_name = name
1299-
self.segment_names = segment_names
1299+
self.segment_names = [] if segment_names is None else segment_names
13001300

13011301
def register_segment(self, segment_name):
13021302
"Adds the supplied shared memory block name to tracker."

Lib/test/test_free_threading/test_io.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,35 @@ def sizeof(barrier, b, *ignore):
122122
class CBytesIOTest(ThreadSafetyMixin, TestCase):
123123
ioclass = io.BytesIO
124124

125+
@threading_helper.requires_working_threading()
126+
@threading_helper.reap_threads
127+
def test_concurrent_whole_buffer_read_and_resize(self):
128+
shared = self.ioclass(b"x" * 64)
129+
writers = 2
130+
readers = 8
131+
loops = 2000
132+
barrier = threading.Barrier(writers + readers)
133+
134+
def writer():
135+
barrier.wait()
136+
for i in range(loops):
137+
shared.seek(0)
138+
shared.write(b"a" * (64 + (i & 63)))
139+
140+
def reader():
141+
barrier.wait()
142+
for _ in range(loops):
143+
shared.seek(0)
144+
shared.read()
145+
shared.seek(0)
146+
shared.peek()
147+
shared.getvalue()
148+
149+
threads = [threading.Thread(target=writer) for _ in range(writers)]
150+
threads += [threading.Thread(target=reader) for _ in range(readers)]
151+
with threading_helper.start_threads(threads):
152+
pass
153+
125154
class PyBytesIOTest(ThreadSafetyMixin, TestCase):
126155
ioclass = pyio.BytesIO
127156

Lib/test/test_imaplib.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ def cmd_AUTHENTICATE(self, tag, args):
420420
self._send_tagged(tag, 'NO', 'No access')
421421

422422

423+
class AuthHandler_PLAIN(SimpleIMAPHandler):
424+
capabilities = 'LOGINDISABLED AUTH=PLAIN'
425+
def cmd_AUTHENTICATE(self, tag, args):
426+
self.server.auth_args = args
427+
self._send_textline('+')
428+
self.server.response = yield
429+
self._send_tagged(tag, 'OK', 'Logged in.')
430+
431+
423432
class NewIMAPTestsMixin:
424433
client = None
425434

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

704+
def test_login_plain_ascii(self):
705+
client, server = self._setup(AuthHandler_PLAIN)
706+
self.assertIn('AUTH=PLAIN', client.capabilities)
707+
ret, _ = client.login_plain("prem", "pass")
708+
self.assertEqual(ret, "OK")
709+
self.assertEqual(server.auth_args, ['PLAIN'])
710+
self.assertEqual(server.response, b'AHByZW0AcGFzcw==\r\n')
711+
712+
def test_login_plain_utf8(self):
713+
client, server = self._setup(AuthHandler_PLAIN)
714+
self.assertIn('AUTH=PLAIN', client.capabilities)
715+
ret, _ = client.login_plain("pręm", "żółć")
716+
self.assertEqual(ret, "OK")
717+
self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')
718+
719+
def test_login_plain_bytes(self):
720+
# bytes are accepted and sent verbatim (not re-encoded).
721+
client, server = self._setup(AuthHandler_PLAIN)
722+
ret, _ = client.login_plain(b'pr\xc4\x99m', b'\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87')
723+
self.assertEqual(ret, "OK")
724+
self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')
725+
726+
def test_login_plain_nul(self):
727+
client, _ = self._setup(AuthHandler_PLAIN)
728+
with self.assertRaises(ValueError):
729+
client.login_plain('user\0', 'pass')
730+
with self.assertRaises(ValueError):
731+
client.login_plain('user', b'pa\0ss')
732+
695733
def test_aborted_authentication(self):
696734
class MyServer(SimpleIMAPHandler):
697735
def cmd_AUTHENTICATE(self, tag, args):
@@ -1701,6 +1739,24 @@ def test_getquotaroot(self):
17011739
self.assertEqual(typ, 'OK')
17021740
self.assertEqual(server.args, ['"New folder"'])
17031741

1742+
def test_id(self):
1743+
client, server = self._setup(make_simple_handler('ID',
1744+
['* ID ("name" "Cyrus" "version" "1.5")']))
1745+
typ, data = client.id({'name': 'imaplib', 'version': '3.16'})
1746+
self.assertEqual(typ, 'OK')
1747+
self.assertEqual(data, [b'("name" "Cyrus" "version" "1.5")'])
1748+
self.assertEqual(server.args, ['("name" "imaplib" "version" "3.16")'])
1749+
1750+
typ, data = client.id()
1751+
self.assertEqual(typ, 'OK')
1752+
self.assertEqual(server.args, ['NIL'])
1753+
1754+
# Fields and values are quoted strings; a None value is sent
1755+
# as NIL.
1756+
typ, data = client.id({'name': 'my "client"', 'os': None})
1757+
self.assertEqual(typ, 'OK')
1758+
self.assertEqual(server.args, [r'("name" "my \"client\"" "os" NIL)'])
1759+
17041760
def test_setquota(self):
17051761
client, server = self._setup(make_simple_handler('SETQUOTA',
17061762
['* QUOTA "" (STORAGE 512)']))

Lib/test/test_io/test_memoryio.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,10 @@ def test_setstate(self):
939939

940940
@support.cpython_only
941941
def test_sizeof(self):
942-
basesize = support.calcobjsize('P2n2Pn')
942+
if support.Py_GIL_DISABLED:
943+
basesize = support.calcobjsize('P2n2Pni')
944+
else:
945+
basesize = support.calcobjsize('P2n2Pn')
943946
check = self.check_sizeof
944947
self.assertEqual(object.__sizeof__(io.BytesIO()), basesize)
945948
check(io.BytesIO(), basesize )

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ Stan Bubrouski
260260
Brandt Bucher
261261
Curtis Bucher
262262
Colm Buckley
263+
Przemysław Buczkowski
263264
Erik de Bueger
264265
Jan-Hein Bührman
265266
Marc Bürg
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a data race in :class:`io.BytesIO` in free-threaded builds when
2+
whole-buffer reads or peeks, or :meth:`~io.BytesIO.getvalue`, share the
3+
internal buffer with concurrent writes.

0 commit comments

Comments
 (0)