55
66"""Basic message object for the email package object model."""
77from __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
1511import 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
2018from future .backports .email import utils
2119from future .backports .email import errors
22- from future .backports .email ._policybase import compat32
2320from future .backports .email import charset as _charset
2421from 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
2726SEMISPACE = '; '
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.
3130tspecials = re .compile (r'[ \(\)<>@,;:\\"/\[\]\?=]' )
3231
@@ -41,6 +40,7 @@ def _splitparam(param):
4140 return a .strip (), None
4241 return a .strip (), b .strip ()
4342
43+
4444def _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+
7879def _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+
109141class 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 ()
0 commit comments