5
5
6
6
"""Basic message object for the email package object model."""
7
7
from __future__ import absolute_import , division , unicode_literals
8
- from future .builtins import list , range , str , zip
9
8
10
9
__all__ = ['Message' ]
11
10
12
- import re
13
- import uu
14
- import base64
15
11
import binascii
16
- from io import BytesIO , StringIO
12
+ import quopri
13
+ import re
14
+ from io import StringIO
17
15
18
16
# Intrapackage imports
19
- from future .utils import as_native_str
17
+ from future .builtins import list , range , str , zip
20
18
from future .backports .email import utils
21
19
from future .backports .email import errors
22
- from future .backports .email ._policybase import compat32
23
20
from future .backports .email import charset as _charset
24
21
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
26
24
25
+ Charset = _charset .Charset
27
26
SEMISPACE = '; '
28
27
29
- # Regular expression that matches ` special' characters in parameters, the
28
+ # Regular expression that matches ' special' characters in parameters, the
30
29
# existence of which force quoting of the parameter value.
31
30
tspecials = re .compile (r'[ \(\)<>@,;:\\"/\[\]\?=]' )
32
31
@@ -41,6 +40,7 @@ def _splitparam(param):
41
40
return a .strip (), None
42
41
return a .strip (), b .strip ()
43
42
43
+
44
44
def _formatparam (param , value = None , quote = True ):
45
45
"""Convenience function to format and return a key=value pair.
46
46
@@ -75,6 +75,7 @@ def _formatparam(param, value=None, quote=True):
75
75
else :
76
76
return param
77
77
78
+
78
79
def _parseparam (s ):
79
80
# RDM This might be a Header, so for now stringify it.
80
81
s = ';' + str (s )
@@ -106,6 +107,37 @@ def _unquotevalue(value):
106
107
return utils .unquote (value )
107
108
108
109
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
+
109
141
class Message (object ):
110
142
"""Basic message object.
111
143
@@ -115,7 +147,7 @@ class Message(object):
115
147
multipart or a message/rfc822), then the payload is a list of Message
116
148
objects, otherwise it is a string.
117
149
118
- Message objects implement part of the ` mapping' interface, which assumes
150
+ Message objects implement part of the ' mapping' interface, which assumes
119
151
there is exactly one occurrence of the header per message. Some headers
120
152
do in fact appear multiple times (e.g. Received) and for those headers,
121
153
you must use the explicit API to set or get all the headers. Not all of
@@ -181,7 +213,11 @@ def attach(self, payload):
181
213
if self ._payload is None :
182
214
self ._payload = [payload ]
183
215
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" )
185
221
186
222
def get_payload (self , i = None , decode = False ):
187
223
"""Return a reference to the payload.
@@ -238,22 +274,22 @@ def get_payload(self, i=None, decode=False):
238
274
bpayload = payload .encode ('ascii' , 'surrogateescape' )
239
275
if not decode :
240
276
try :
241
- payload = bpayload .decode (self .get_param ( 'charset' , 'ascii' ), 'replace' )
277
+ payload = bpayload .decode (self .get_content_charset ( 'ascii' ), 'replace' )
242
278
except LookupError :
243
279
payload = bpayload .decode ('ascii' , 'replace' )
244
280
elif decode :
245
281
try :
246
282
bpayload = payload .encode ('ascii' )
247
283
except UnicodeError :
248
284
# 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).
250
286
# If it does happen, turn the string into bytes in a way
251
287
# guaranteed not to fail.
252
288
bpayload = payload .encode ('raw-unicode-escape' )
253
289
if not decode :
254
290
return payload
255
291
if cte == 'quoted-printable' :
256
- return utils . _qdecode (bpayload )
292
+ return quopri . decodestring (bpayload )
257
293
elif cte == 'base64' :
258
294
# XXX: this is a bit of a hack; decode_b should probably be factored
259
295
# out somewhere, but I haven't figured out where yet.
@@ -262,13 +298,10 @@ def get_payload(self, i=None, decode=False):
262
298
self .policy .handle_defect (self , defect )
263
299
return value
264
300
elif cte in ('x-uuencode' , 'uuencode' , 'uue' , 'x-uue' ):
265
- in_file = BytesIO (bpayload )
266
- out_file = BytesIO ()
267
301
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.
272
305
return bpayload
273
306
if isinstance (payload , str ):
274
307
return bpayload
@@ -355,7 +388,7 @@ def __setitem__(self, name, val):
355
388
if max_count :
356
389
lname = name .lower ()
357
390
found = 0
358
- for k , v in self ._headers :
391
+ for k , _ in self ._headers :
359
392
if k .lower () == lname :
360
393
found += 1
361
394
if found >= max_count :
@@ -376,10 +409,14 @@ def __delitem__(self, name):
376
409
self ._headers = newheaders
377
410
378
411
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
380
417
381
418
def __iter__ (self ):
382
- for field , value in self ._headers :
419
+ for field , _ in self ._headers :
383
420
yield field
384
421
385
422
def keys (self ):
@@ -505,7 +542,7 @@ def replace_header(self, _name, _value):
505
542
raised.
506
543
"""
507
544
_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 ):
509
546
if k .lower () == _name :
510
547
self ._headers [i ] = self .policy .header_store_parse (k , _value )
511
548
break
@@ -520,7 +557,7 @@ def get_content_type(self):
520
557
"""Return the message's content type.
521
558
522
559
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
524
561
message, the default type as given by get_default_type() will be
525
562
returned. Since according to RFC 2045, messages always have a default
526
563
type this will always return a value.
@@ -543,7 +580,7 @@ def get_content_type(self):
543
580
def get_content_maintype (self ):
544
581
"""Return the message's main content type.
545
582
546
- This is the ` maintype' part of the string returned by
583
+ This is the ' maintype' part of the string returned by
547
584
get_content_type().
548
585
"""
549
586
ctype = self .get_content_type ()
@@ -552,14 +589,14 @@ def get_content_maintype(self):
552
589
def get_content_subtype (self ):
553
590
"""Returns the message's sub-content type.
554
591
555
- This is the ` subtype' part of the string returned by
592
+ This is the ' subtype' part of the string returned by
556
593
get_content_type().
557
594
"""
558
595
ctype = self .get_content_type ()
559
596
return ctype .split ('/' )[1 ]
560
597
561
598
def get_default_type (self ):
562
- """Return the ` default' content type.
599
+ """Return the ' default' content type.
563
600
564
601
Most messages have a default content type of text/plain, except for
565
602
messages that are subparts of multipart/digest containers. Such
@@ -568,7 +605,7 @@ def get_default_type(self):
568
605
return self ._default_type
569
606
570
607
def set_default_type (self , ctype ):
571
- """Set the ` default' content type.
608
+ """Set the ' default' content type.
572
609
573
610
ctype should be either "text/plain" or "message/rfc822", although this
574
611
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):
601
638
"""Return the message's Content-Type parameters, as a list.
602
639
603
640
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
606
643
the parameter the value is the empty string. The value is as
607
644
described in the get_param() method.
608
645
@@ -664,7 +701,7 @@ def set_param(self, param, value, header='Content-Type', requote=True,
664
701
message, it will be set to "text/plain" and the new parameter and
665
702
value will be appended as per RFC 2045.
666
703
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
668
705
parameters will be quoted as necessary unless requote is False.
669
706
670
707
If charset is specified, the parameter will be encoded according to RFC
@@ -759,9 +796,9 @@ def get_filename(self, failobj=None):
759
796
"""Return the filename associated with the payload if present.
760
797
761
798
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.
765
802
"""
766
803
missing = object ()
767
804
filename = self .get_param ('filename' , missing , 'content-disposition' )
@@ -774,7 +811,7 @@ def get_filename(self, failobj=None):
774
811
def get_boundary (self , failobj = None ):
775
812
"""Return the boundary associated with the payload if present.
776
813
777
- The boundary is extracted from the Content-Type header's ` boundary'
814
+ The boundary is extracted from the Content-Type header's ' boundary'
778
815
parameter, and it is unquoted.
779
816
"""
780
817
missing = object ()
0 commit comments