Skip to content

Commit 6cbd877

Browse files
committed
Remove use of the uu standard library module
which is being removed in Python 3.13, by backporting python/cpython@407c3af Partial fix for #640 . Also: - Backport small fixes from upstream `email` library module - Remove unused imports
1 parent 4311bfc commit 6cbd877

File tree

2 files changed

+81
-48
lines changed

2 files changed

+81
-48
lines changed

src/future/backports/email/message.py

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,27 @@
55

66
"""Basic message object for the email package object model."""
77
from __future__ import absolute_import, division, unicode_literals
8-
from future.builtins import list, range, str, zip
98

109
__all__ = ['Message']
1110

12-
import re
13-
import uu
14-
import base64
1511
import binascii
16-
from io import BytesIO, StringIO
12+
import quopri
13+
import re
14+
from io import StringIO
1715

1816
# Intrapackage imports
19-
from future.utils import as_native_str
17+
from future.builtins import list, range, str, zip
2018
from future.backports.email import utils
2119
from future.backports.email import errors
22-
from future.backports.email._policybase import compat32
2320
from future.backports.email import charset as _charset
2421
from future.backports.email._encoded_words import decode_b
25-
Charset = _charset.Charset
22+
from future.backports.email._policybase import compat32
23+
from future.utils import as_native_str
2624

25+
Charset = _charset.Charset
2726
SEMISPACE = '; '
2827

29-
# Regular expression that matches `special' characters in parameters, the
28+
# Regular expression that matches 'special' characters in parameters, the
3029
# existence of which force quoting of the parameter value.
3130
tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
3231

@@ -41,6 +40,7 @@ def _splitparam(param):
4140
return a.strip(), None
4241
return a.strip(), b.strip()
4342

43+
4444
def _formatparam(param, value=None, quote=True):
4545
"""Convenience function to format and return a key=value pair.
4646
@@ -75,6 +75,7 @@ def _formatparam(param, value=None, quote=True):
7575
else:
7676
return param
7777

78+
7879
def _parseparam(s):
7980
# RDM This might be a Header, so for now stringify it.
8081
s = ';' + str(s)
@@ -106,6 +107,37 @@ def _unquotevalue(value):
106107
return utils.unquote(value)
107108

108109

110+
def _decode_uu(encoded):
111+
"""Decode uuencoded data."""
112+
decoded_lines = []
113+
encoded_lines_iter = iter(encoded.splitlines())
114+
for line in encoded_lines_iter:
115+
if line.startswith(b"begin "):
116+
mode, _, path = line.removeprefix(b"begin ").partition(b" ")
117+
try:
118+
int(mode, base=8)
119+
except ValueError:
120+
continue
121+
else:
122+
break
123+
else:
124+
raise ValueError("`begin` line not found")
125+
for line in encoded_lines_iter:
126+
if not line:
127+
raise ValueError("Truncated input")
128+
elif line.strip(b' \t\r\n\f') == b'end':
129+
break
130+
try:
131+
decoded_line = binascii.a2b_uu(line)
132+
except binascii.Error:
133+
# Workaround for broken uuencoders by /Fredrik Lundh
134+
nbytes = (((line[0]-32) & 63) * 4 + 5) // 3
135+
decoded_line = binascii.a2b_uu(line[:nbytes])
136+
decoded_lines.append(decoded_line)
137+
138+
return b''.join(decoded_lines)
139+
140+
109141
class Message(object):
110142
"""Basic message object.
111143
@@ -115,7 +147,7 @@ class Message(object):
115147
multipart or a message/rfc822), then the payload is a list of Message
116148
objects, otherwise it is a string.
117149
118-
Message objects implement part of the `mapping' interface, which assumes
150+
Message objects implement part of the 'mapping' interface, which assumes
119151
there is exactly one occurrence of the header per message. Some headers
120152
do in fact appear multiple times (e.g. Received) and for those headers,
121153
you must use the explicit API to set or get all the headers. Not all of
@@ -181,7 +213,11 @@ def attach(self, payload):
181213
if self._payload is None:
182214
self._payload = [payload]
183215
else:
184-
self._payload.append(payload)
216+
try:
217+
self._payload.append(payload)
218+
except AttributeError:
219+
raise TypeError("Attach is not valid on a message with a"
220+
" non-multipart payload")
185221

186222
def get_payload(self, i=None, decode=False):
187223
"""Return a reference to the payload.
@@ -238,22 +274,22 @@ def get_payload(self, i=None, decode=False):
238274
bpayload = payload.encode('ascii', 'surrogateescape')
239275
if not decode:
240276
try:
241-
payload = bpayload.decode(self.get_param('charset', 'ascii'), 'replace')
277+
payload = bpayload.decode(self.get_content_charset('ascii'), 'replace')
242278
except LookupError:
243279
payload = bpayload.decode('ascii', 'replace')
244280
elif decode:
245281
try:
246282
bpayload = payload.encode('ascii')
247283
except UnicodeError:
248284
# This won't happen for RFC compliant messages (messages
249-
# containing only ASCII codepoints in the unicode input).
285+
# containing only ASCII code points in the unicode input).
250286
# If it does happen, turn the string into bytes in a way
251287
# guaranteed not to fail.
252288
bpayload = payload.encode('raw-unicode-escape')
253289
if not decode:
254290
return payload
255291
if cte == 'quoted-printable':
256-
return utils._qdecode(bpayload)
292+
return quopri.decodestring(bpayload)
257293
elif cte == 'base64':
258294
# XXX: this is a bit of a hack; decode_b should probably be factored
259295
# out somewhere, but I haven't figured out where yet.
@@ -262,13 +298,10 @@ def get_payload(self, i=None, decode=False):
262298
self.policy.handle_defect(self, defect)
263299
return value
264300
elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
265-
in_file = BytesIO(bpayload)
266-
out_file = BytesIO()
267301
try:
268-
uu.decode(in_file, out_file, quiet=True)
269-
return out_file.getvalue()
270-
except uu.Error:
271-
# Some decoding problem
302+
return _decode_uu(bpayload)
303+
except ValueError:
304+
# Some decoding problem.
272305
return bpayload
273306
if isinstance(payload, str):
274307
return bpayload
@@ -355,7 +388,7 @@ def __setitem__(self, name, val):
355388
if max_count:
356389
lname = name.lower()
357390
found = 0
358-
for k, v in self._headers:
391+
for k, _ in self._headers:
359392
if k.lower() == lname:
360393
found += 1
361394
if found >= max_count:
@@ -376,10 +409,14 @@ def __delitem__(self, name):
376409
self._headers = newheaders
377410

378411
def __contains__(self, name):
379-
return name.lower() in [k.lower() for k, v in self._headers]
412+
name_lower = name.lower()
413+
for k, _ in self._headers:
414+
if name_lower == k.lower():
415+
return True
416+
return False
380417

381418
def __iter__(self):
382-
for field, value in self._headers:
419+
for field, _ in self._headers:
383420
yield field
384421

385422
def keys(self):
@@ -505,7 +542,7 @@ def replace_header(self, _name, _value):
505542
raised.
506543
"""
507544
_name = _name.lower()
508-
for i, (k, v) in zip(range(len(self._headers)), self._headers):
545+
for i, (k, _) in zip(range(len(self._headers)), self._headers):
509546
if k.lower() == _name:
510547
self._headers[i] = self.policy.header_store_parse(k, _value)
511548
break
@@ -520,7 +557,7 @@ def get_content_type(self):
520557
"""Return the message's content type.
521558
522559
The returned string is coerced to lower case of the form
523-
`maintype/subtype'. If there was no Content-Type header in the
560+
'maintype/subtype'. If there was no Content-Type header in the
524561
message, the default type as given by get_default_type() will be
525562
returned. Since according to RFC 2045, messages always have a default
526563
type this will always return a value.
@@ -543,7 +580,7 @@ def get_content_type(self):
543580
def get_content_maintype(self):
544581
"""Return the message's main content type.
545582
546-
This is the `maintype' part of the string returned by
583+
This is the 'maintype' part of the string returned by
547584
get_content_type().
548585
"""
549586
ctype = self.get_content_type()
@@ -552,14 +589,14 @@ def get_content_maintype(self):
552589
def get_content_subtype(self):
553590
"""Returns the message's sub-content type.
554591
555-
This is the `subtype' part of the string returned by
592+
This is the 'subtype' part of the string returned by
556593
get_content_type().
557594
"""
558595
ctype = self.get_content_type()
559596
return ctype.split('/')[1]
560597

561598
def get_default_type(self):
562-
"""Return the `default' content type.
599+
"""Return the 'default' content type.
563600
564601
Most messages have a default content type of text/plain, except for
565602
messages that are subparts of multipart/digest containers. Such
@@ -568,7 +605,7 @@ def get_default_type(self):
568605
return self._default_type
569606

570607
def set_default_type(self, ctype):
571-
"""Set the `default' content type.
608+
"""Set the 'default' content type.
572609
573610
ctype should be either "text/plain" or "message/rfc822", although this
574611
is not enforced. The default content type is not stored in the
@@ -601,8 +638,8 @@ def get_params(self, failobj=None, header='content-type', unquote=True):
601638
"""Return the message's Content-Type parameters, as a list.
602639
603640
The elements of the returned list are 2-tuples of key/value pairs, as
604-
split on the `=' sign. The left hand side of the `=' is the key,
605-
while the right hand side is the value. If there is no `=' sign in
641+
split on the '=' sign. The left hand side of the '=' is the key,
642+
while the right hand side is the value. If there is no '=' sign in
606643
the parameter the value is the empty string. The value is as
607644
described in the get_param() method.
608645
@@ -664,7 +701,7 @@ def set_param(self, param, value, header='Content-Type', requote=True,
664701
message, it will be set to "text/plain" and the new parameter and
665702
value will be appended as per RFC 2045.
666703
667-
An alternate header can specified in the header argument, and all
704+
An alternate header can be specified in the header argument, and all
668705
parameters will be quoted as necessary unless requote is False.
669706
670707
If charset is specified, the parameter will be encoded according to RFC
@@ -759,9 +796,9 @@ def get_filename(self, failobj=None):
759796
"""Return the filename associated with the payload if present.
760797
761798
The filename is extracted from the Content-Disposition header's
762-
`filename' parameter, and it is unquoted. If that header is missing
763-
the `filename' parameter, this method falls back to looking for the
764-
`name' parameter.
799+
'filename' parameter, and it is unquoted. If that header is missing
800+
the 'filename' parameter, this method falls back to looking for the
801+
'name' parameter.
765802
"""
766803
missing = object()
767804
filename = self.get_param('filename', missing, 'content-disposition')
@@ -774,7 +811,7 @@ def get_filename(self, failobj=None):
774811
def get_boundary(self, failobj=None):
775812
"""Return the boundary associated with the payload if present.
776813
777-
The boundary is extracted from the Content-Type header's `boundary'
814+
The boundary is extracted from the Content-Type header's 'boundary'
778815
parameter, and it is unquoted.
779816
"""
780817
missing = object()

src/future/backports/email/utils.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,18 @@
3333
if utils.PY2:
3434
re.ASCII = 0
3535
import time
36-
import base64
3736
import random
3837
import socket
3938
from future.backports import datetime
4039
from future.backports.urllib.parse import quote as url_quote, unquote as url_unquote
41-
import warnings
42-
from io import StringIO
4340

4441
from future.backports.email._parseaddr import quote
4542
from future.backports.email._parseaddr import AddressList as _AddressList
4643
from future.backports.email._parseaddr import mktime_tz
4744

4845
from future.backports.email._parseaddr import parsedate, parsedate_tz, _parsedate_tz
4946

50-
from quopri import decodestring as _qdecode
51-
5247
# Intrapackage imports
53-
from future.backports.email.encoders import _bencode, _qencode
5448
from future.backports.email.charset import Charset
5549

5650
COMMASPACE = ', '
@@ -67,6 +61,7 @@
6761
_has_surrogates = re.compile(
6862
'([^\ud800-\udbff]|\A)[\udc00-\udfff]([^\udc00-\udfff]|\Z)').search
6963

64+
7065
# How to deal with a string containing bytes before handing it to the
7166
# application through the 'normal' interface.
7267
def _sanitize(string):
@@ -85,13 +80,13 @@ def formataddr(pair, charset='utf-8'):
8580
If the first element of pair is false, then the second element is
8681
returned unmodified.
8782
88-
Optional charset if given is the character set that is used to encode
83+
The optional charset is the character set that is used to encode
8984
realname in case realname is not ASCII safe. Can be an instance of str or
9085
a Charset-like object which has a header_encode method. Default is
9186
'utf-8'.
9287
"""
9388
name, address = pair
94-
# The address MUST (per RFC) be ascii, so raise an UnicodeError if it isn't.
89+
# The address MUST (per RFC) be ascii, so raise a UnicodeError if it isn't.
9590
address.encode('ascii')
9691
if name:
9792
try:
@@ -110,15 +105,13 @@ def formataddr(pair, charset='utf-8'):
110105
return address
111106

112107

113-
114108
def getaddresses(fieldvalues):
115109
"""Return a list of (REALNAME, EMAIL) for each fieldvalue."""
116110
all = COMMASPACE.join(fieldvalues)
117111
a = _AddressList(all)
118112
return a.addresslist
119113

120114

121-
122115
ecre = re.compile(r'''
123116
=\? # literal =?
124117
(?P<charset>[^?]*?) # non-greedy up to the next ? is the charset
@@ -139,12 +132,13 @@ def _format_timetuple_and_zone(timetuple, zone):
139132
timetuple[0], timetuple[3], timetuple[4], timetuple[5],
140133
zone)
141134

135+
142136
def formatdate(timeval=None, localtime=False, usegmt=False):
143137
"""Returns a date string as specified by RFC 2822, e.g.:
144138
145139
Fri, 09 Nov 2001 01:08:47 -0000
146140
147-
Optional timeval if given is a floating point time value as accepted by
141+
Optional timeval if given is a floating-point time value as accepted by
148142
gmtime() and localtime(), otherwise the current time is used.
149143
150144
Optional localtime is a flag that when True, interprets timeval, and
@@ -184,6 +178,7 @@ def formatdate(timeval=None, localtime=False, usegmt=False):
184178
zone = '-0000'
185179
return _format_timetuple_and_zone(now, zone)
186180

181+
187182
def format_datetime(dt, usegmt=False):
188183
"""Turn a datetime into a date string as specified in RFC 2822.
189184
@@ -254,7 +249,6 @@ def unquote(str):
254249
return str
255250

256251

257-
258252
# RFC2231-related functions - parameter encoding and decoding
259253
def decode_rfc2231(s):
260254
"""Decode string according to RFC 2231"""
@@ -282,6 +276,7 @@ def encode_rfc2231(s, charset=None, language=None):
282276
rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$',
283277
re.ASCII)
284278

279+
285280
def decode_params(params):
286281
"""Decode parameters list according to RFC 2231.
287282
@@ -338,6 +333,7 @@ def decode_params(params):
338333
new_params.append((name, '"%s"' % value))
339334
return new_params
340335

336+
341337
def collapse_rfc2231_value(value, errors='replace',
342338
fallback_charset='us-ascii'):
343339
if not isinstance(value, tuple) or len(value) != 3:

0 commit comments

Comments
 (0)