Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a6429db

Browse files
committed
#21800: Add RFC 6855 support to imaplib.
Original patch by Milan Oberkirch, updated by myself and Maciej Szulik.
1 parent 18c30a2 commit a6429db

5 files changed

Lines changed: 179 additions & 21 deletions

File tree

Doc/library/imaplib.rst

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ Three exceptions are defined as attributes of the :class:`IMAP4` class:
7777
There's also a subclass for secure connections:
7878

7979

80-
.. class:: IMAP4_SSL(host='', port=IMAP4_SSL_PORT, keyfile=None, certfile=None, ssl_context=None)
80+
.. class:: IMAP4_SSL(host='', port=IMAP4_SSL_PORT, keyfile=None, \
81+
certfile=None, ssl_context=None)
8182

8283
This is a subclass derived from :class:`IMAP4` that connects over an SSL
8384
encrypted socket (to use this class you need a socket module that was compiled
@@ -211,6 +212,10 @@ An :class:`IMAP4` instance has the following methods:
211212
that will be base64 encoded and sent to the server. It should return
212213
``None`` if the client abort response ``*`` should be sent instead.
213214

215+
.. versionchanged:: 3.5
216+
string usernames and passwords are now encoded to ``utf-8`` instead of
217+
being limited to ASCII.
218+
214219

215220
.. method:: IMAP4.check()
216221

@@ -243,6 +248,16 @@ An :class:`IMAP4` instance has the following methods:
243248
Delete the ACLs (remove any rights) set for who on mailbox.
244249

245250

251+
.. method:: IMAP4.enable(capability)
252+
253+
Enable *capability* (see :rfc:`5161`). Most capabilities do not need to be
254+
enabled. Currently only the ``UTF8=ACCEPT`` capability is supported
255+
(see :RFC:`6855`).
256+
257+
.. versionadded:: 3.5
258+
The :meth:`enable` method itself, and :RFC:`6855` support.
259+
260+
246261
.. method:: IMAP4.expunge()
247262

248263
Permanently remove deleted items from selected mailbox. Generates an ``EXPUNGE``
@@ -380,7 +395,9 @@ An :class:`IMAP4` instance has the following methods:
380395
Search mailbox for matching messages. *charset* may be ``None``, in which case
381396
no ``CHARSET`` will be specified in the request to the server. The IMAP
382397
protocol requires that at least one criterion be specified; an exception will be
383-
raised when the server returns an error.
398+
raised when the server returns an error. *charset* must be ``None`` if
399+
the ``UTF8=ACCEPT`` capability was enabled using the :meth:`enable`
400+
command.
384401

385402
Example::
386403

@@ -542,6 +559,15 @@ The following attributes are defined on instances of :class:`IMAP4`:
542559
the module variable ``Debug``. Values greater than three trace each command.
543560

544561

562+
.. attribute:: IMAP4.utf8_enabled
563+
564+
Boolean value that is normally ``False``, but is set to ``True`` if an
565+
:meth:`enable` command is successfully issued for the ``UTF8=ACCEPT``
566+
capability.
567+
568+
.. versionadded:: 3.5
569+
570+
545571
.. _imap4-example:
546572

547573
IMAP4 Example

Doc/whatsnew/3.5.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,17 @@ imaplib
337337
automatically at the end of the block. (Contributed by Tarek Ziadé and
338338
Serhiy Storchaka in :issue:`4972`.)
339339

340+
* :mod:`imaplib` now supports :rfc:`5161`: the :meth:`~imaplib.IMAP4.enable`
341+
extension), and :rfc:`6855`: utf-8 support (internationalized email, via the
342+
``UTF8=ACCEPT`` argument to :meth:`~imaplib.IMAP4.enable`). A new attribute,
343+
:attr:`~imaplib.IMAP4.utf8_enabled`, tracks whether or not :rfc:`6855`
344+
support is enabled. Milan Oberkirch, R. David Murray, and Maciej Szulik in
345+
:issue:`21800`.)
346+
347+
* :mod:`imaplib` now automatically encodes non-ASCII string usernames and
348+
passwords using ``UTF8``, as recommended by the RFCs. (Contributed by Milan
349+
Oberkirch in :issue:`21800`.)
350+
340351
imghdr
341352
------
342353

Lib/imaplib.py

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
'CREATE': ('AUTH', 'SELECTED'),
6767
'DELETE': ('AUTH', 'SELECTED'),
6868
'DELETEACL': ('AUTH', 'SELECTED'),
69+
'ENABLE': ('AUTH', ),
6970
'EXAMINE': ('AUTH', 'SELECTED'),
7071
'EXPUNGE': ('SELECTED',),
7172
'FETCH': ('SELECTED',),
@@ -107,12 +108,17 @@
107108
br' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
108109
br' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
109110
br'"')
111+
# Literal is no longer used; kept for backward compatibility.
110112
Literal = re.compile(br'.*{(?P<size>\d+)}$', re.ASCII)
111113
MapCRLF = re.compile(br'\r\n|\r|\n')
112114
Response_code = re.compile(br'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
113115
Untagged_response = re.compile(br'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
116+
# Untagged_status is no longer used; kept for backward compatibility
114117
Untagged_status = re.compile(
115118
br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?', re.ASCII)
119+
# We compile these in _mode_xxx.
120+
_Literal = br'.*{(?P<size>\d+)}$'
121+
_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'
116122

117123

118124

@@ -166,7 +172,7 @@ class error(Exception): pass # Logical errors - debug required
166172
class abort(error): pass # Service errors - close and retry
167173
class readonly(abort): pass # Mailbox status changed to READ-ONLY
168174

169-
def __init__(self, host = '', port = IMAP4_PORT):
175+
def __init__(self, host='', port=IMAP4_PORT):
170176
self.debug = Debug
171177
self.state = 'LOGOUT'
172178
self.literal = None # A literal argument to a command
@@ -176,6 +182,7 @@ def __init__(self, host = '', port = IMAP4_PORT):
176182
self.is_readonly = False # READ-ONLY desired state
177183
self.tagnum = 0
178184
self._tls_established = False
185+
self._mode_ascii()
179186

180187
# Open socket to server.
181188

@@ -190,6 +197,19 @@ def __init__(self, host = '', port = IMAP4_PORT):
190197
pass
191198
raise
192199

200+
def _mode_ascii(self):
201+
self.utf8_enabled = False
202+
self._encoding = 'ascii'
203+
self.Literal = re.compile(_Literal, re.ASCII)
204+
self.Untagged_status = re.compile(_Untagged_status, re.ASCII)
205+
206+
207+
def _mode_utf8(self):
208+
self.utf8_enabled = True
209+
self._encoding = 'utf-8'
210+
self.Literal = re.compile(_Literal)
211+
self.Untagged_status = re.compile(_Untagged_status)
212+
193213

194214
def _connect(self):
195215
# Create unique tag for this session,
@@ -360,7 +380,10 @@ def append(self, mailbox, flags, date_time, message):
360380
date_time = Time2Internaldate(date_time)
361381
else:
362382
date_time = None
363-
self.literal = MapCRLF.sub(CRLF, message)
383+
literal = MapCRLF.sub(CRLF, message)
384+
if self.utf8_enabled:
385+
literal = b'UTF8 (' + literal + b')'
386+
self.literal = literal
364387
return self._simple_command(name, mailbox, flags, date_time)
365388

366389

@@ -455,6 +478,18 @@ def deleteacl(self, mailbox, who):
455478
"""
456479
return self._simple_command('DELETEACL', mailbox, who)
457480

481+
def enable(self, capability):
482+
"""Send an RFC5161 enable string to the server.
483+
484+
(typ, [data]) = <intance>.enable(capability)
485+
"""
486+
if 'ENABLE' not in self.capabilities:
487+
raise IMAP4.error("Server does not support ENABLE")
488+
typ, data = self._simple_command('ENABLE', capability)
489+
if typ == 'OK' and 'UTF8=ACCEPT' in capability.upper():
490+
self._mode_utf8()
491+
return typ, data
492+
458493
def expunge(self):
459494
"""Permanently remove deleted items from selected mailbox.
460495
@@ -561,7 +596,7 @@ def login_cram_md5(self, user, password):
561596
def _CRAM_MD5_AUTH(self, challenge):
562597
""" Authobject to use with CRAM-MD5 authentication. """
563598
import hmac
564-
pwd = (self.password.encode('ASCII') if isinstance(self.password, str)
599+
pwd = (self.password.encode('utf-8') if isinstance(self.password, str)
565600
else self.password)
566601
return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest()
567602

@@ -661,9 +696,12 @@ def search(self, charset, *criteria):
661696
(typ, [data]) = <instance>.search(charset, criterion, ...)
662697
663698
'data' is space separated list of matching message numbers.
699+
If UTF8 is enabled, charset MUST be None.
664700
"""
665701
name = 'SEARCH'
666702
if charset:
703+
if self.utf8_enabled:
704+
raise IMAP4.error("Non-None charset not valid in UTF8 mode")
667705
typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
668706
else:
669707
typ, dat = self._simple_command(name, *criteria)
@@ -877,7 +915,7 @@ def _append_untagged(self, typ, dat):
877915
def _check_bye(self):
878916
bye = self.untagged_responses.get('BYE')
879917
if bye:
880-
raise self.abort(bye[-1].decode('ascii', 'replace'))
918+
raise self.abort(bye[-1].decode(self._encoding, 'replace'))
881919

882920

883921
def _command(self, name, *args):
@@ -898,12 +936,12 @@ def _command(self, name, *args):
898936
raise self.readonly('mailbox status changed to READ-ONLY')
899937

900938
tag = self._new_tag()
901-
name = bytes(name, 'ASCII')
939+
name = bytes(name, self._encoding)
902940
data = tag + b' ' + name
903941
for arg in args:
904942
if arg is None: continue
905943
if isinstance(arg, str):
906-
arg = bytes(arg, "ASCII")
944+
arg = bytes(arg, self._encoding)
907945
data = data + b' ' + arg
908946

909947
literal = self.literal
@@ -913,7 +951,7 @@ def _command(self, name, *args):
913951
literator = literal
914952
else:
915953
literator = None
916-
data = data + bytes(' {%s}' % len(literal), 'ASCII')
954+
data = data + bytes(' {%s}' % len(literal), self._encoding)
917955

918956
if __debug__:
919957
if self.debug >= 4:
@@ -978,7 +1016,7 @@ def _get_capabilities(self):
9781016
typ, dat = self.capability()
9791017
if dat == [None]:
9801018
raise self.error('no CAPABILITY response from server')
981-
dat = str(dat[-1], "ASCII")
1019+
dat = str(dat[-1], self._encoding)
9821020
dat = dat.upper()
9831021
self.capabilities = tuple(dat.split())
9841022

@@ -997,10 +1035,10 @@ def _get_response(self):
9971035
if self._match(self.tagre, resp):
9981036
tag = self.mo.group('tag')
9991037
if not tag in self.tagged_commands:
1000-
raise self.abort('unexpected tagged response: %s' % resp)
1038+
raise self.abort('unexpected tagged response: %r' % resp)
10011039

10021040
typ = self.mo.group('type')
1003-
typ = str(typ, 'ASCII')
1041+
typ = str(typ, self._encoding)
10041042
dat = self.mo.group('data')
10051043
self.tagged_commands[tag] = (typ, [dat])
10061044
else:
@@ -1009,7 +1047,7 @@ def _get_response(self):
10091047
# '*' (untagged) responses?
10101048

10111049
if not self._match(Untagged_response, resp):
1012-
if self._match(Untagged_status, resp):
1050+
if self._match(self.Untagged_status, resp):
10131051
dat2 = self.mo.group('data2')
10141052

10151053
if self.mo is None:
@@ -1019,17 +1057,17 @@ def _get_response(self):
10191057
self.continuation_response = self.mo.group('data')
10201058
return None # NB: indicates continuation
10211059

1022-
raise self.abort("unexpected response: '%s'" % resp)
1060+
raise self.abort("unexpected response: %r" % resp)
10231061

10241062
typ = self.mo.group('type')
1025-
typ = str(typ, 'ascii')
1063+
typ = str(typ, self._encoding)
10261064
dat = self.mo.group('data')
10271065
if dat is None: dat = b'' # Null untagged response
10281066
if dat2: dat = dat + b' ' + dat2
10291067

10301068
# Is there a literal to come?
10311069

1032-
while self._match(Literal, dat):
1070+
while self._match(self.Literal, dat):
10331071

10341072
# Read literal direct from connection.
10351073

@@ -1053,7 +1091,7 @@ def _get_response(self):
10531091

10541092
if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
10551093
typ = self.mo.group('type')
1056-
typ = str(typ, "ASCII")
1094+
typ = str(typ, self._encoding)
10571095
self._append_untagged(typ, self.mo.group('data'))
10581096

10591097
if __debug__:
@@ -1123,7 +1161,7 @@ def _match(self, cre, s):
11231161

11241162
def _new_tag(self):
11251163

1126-
tag = self.tagpre + bytes(str(self.tagnum), 'ASCII')
1164+
tag = self.tagpre + bytes(str(self.tagnum), self._encoding)
11271165
self.tagnum = self.tagnum + 1
11281166
self.tagged_commands[tag] = None
11291167
return tag
@@ -1213,7 +1251,8 @@ class IMAP4_SSL(IMAP4):
12131251
"""
12141252

12151253

1216-
def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None, certfile=None, ssl_context=None):
1254+
def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None,
1255+
certfile=None, ssl_context=None):
12171256
if ssl_context is not None and keyfile is not None:
12181257
raise ValueError("ssl_context and keyfile arguments are mutually "
12191258
"exclusive")
@@ -1251,7 +1290,7 @@ class IMAP4_stream(IMAP4):
12511290
12521291
Instantiate with: IMAP4_stream(command)
12531292
1254-
where "command" is a string that can be passed to subprocess.Popen()
1293+
"command" - a string that can be passed to subprocess.Popen()
12551294
12561295
for more documentation see the docstring of the parent class IMAP4.
12571296
"""
@@ -1328,7 +1367,7 @@ def encode(self, inp):
13281367
#
13291368
oup = b''
13301369
if isinstance(inp, str):
1331-
inp = inp.encode('ASCII')
1370+
inp = inp.encode('utf-8')
13321371
while inp:
13331372
if len(inp) > 48:
13341373
t = inp[:48]

0 commit comments

Comments
 (0)