# Copyright (C) 2001-2007 Python Software Foundation# Author: Barry Warsaw# Contact: email-sig@python.org"""Basic message object for the email package object model."""__all__ = ['Message', 'EmailMessage']import reimport uuimport quoprifrom io import BytesIO, StringIO# Intrapackage importsfrom email import utilsfrom email import errorsfrom email._policybase import Policy, compat32from email import charset as _charsetfrom email._encoded_words import decode_bCharset = _charset.CharsetSEMISPACE = '; '# Regular expression that matches `special' characters in parameters, the# existence of which force quoting of the parameter value.tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')def _splitparam(param):# Split header parameters. BAW: this may be too simple. It isn't# strictly RFC 2045 (section 5.1) compliant, but it catches most headers# found in the wild. We may eventually need a full fledged parser.# RDM: we might have a Header here; for now just stringify it.a, sep, b = str(param).partition(';')if not sep:return a.strip(), Nonereturn a.strip(), b.strip()def _formatparam(param, value=None, quote=True):"""Convenience function to format and return a key=value pair.This will quote the value if needed or if quote is true. If value is athree tuple (charset, language, value), it will be encoded accordingto RFC2231 rules. If it contains non-ascii characters it will likewisebe encoded according to RFC2231 rules, using the utf-8 charset anda null language."""if value is not None and len(value) > 0:# A tuple is used for RFC 2231 encoded parameter values where items# are (charset, language, value). charset is a string, not a Charset# instance. RFC 2231 encoded values are never quoted, per RFC.if isinstance(value, tuple):# Encode as per RFC 2231param += '*'value = utils.encode_rfc2231(value[2], value[0], value[1])return '%s=%s' % (param, value)else:try:value.encode('ascii')except UnicodeEncodeError:param += '*'value = utils.encode_rfc2231(value, 'utf-8', '')return '%s=%s' % (param, value)# BAW: Please check this. I think that if quote is set it should# force quoting even if not necessary.if quote or tspecials.search(value):return '%s="%s"' % (param, utils.quote(value))else:return '%s=%s' % (param, value)else:return paramdef _parseparam(s):# RDM This might be a Header, so for now stringify it.s = ';' + str(s)plist = []while s[:1] == ';':s = s[1:]end = s.find(';')while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:end = s.find(';', end + 1)if end < 0:end = len(s)f = s[:end]if '=' in f:i = f.index('=')f = f[:i].strip().lower() + '=' + f[i+1:].strip()plist.append(f.strip())s = s[end:]return plistdef _unquotevalue(value):# This is different than utils.collapse_rfc2231_value() because it doesn't# try to convert the value to a unicode. Message.get_param() and# Message.get_params() are both currently defined to return the tuple in# the face of RFC 2231 parameters.if isinstance(value, tuple):return value[0], value[1], utils.unquote(value[2])else:return utils.unquote(value)class Message:"""Basic message object.A message object is defined as something that has a bunch of RFC 2822headers and a payload. It may optionally have an envelope header(a.k.a. Unix-From or From_ header). If the message is a container (i.e. amultipart or a message/rfc822), then the payload is a list of Messageobjects, otherwise it is a string.Message objects implement part of the `mapping' interface, which assumesthere is exactly one occurrence of the header per message. Some headersdo in fact appear multiple times (e.g. Received) and for those headers,you must use the explicit API to set or get all the headers. Not all ofthe mapping methods are implemented."""def __init__(self, policy=compat32):self.policy = policyself._headers = []self._unixfrom = Noneself._payload = Noneself._charset = None# Defaults for multipart messagesself.preamble = self.epilogue = Noneself.defects = []# Default content typeself._default_type = 'text/plain'def __str__(self):"""Return the entire formatted message as a string."""return self.as_string()def as_string(self, unixfrom=False, maxheaderlen=0, policy=None):"""Return the entire formatted message as a string.Optional 'unixfrom', when true, means include the Unix From_ envelopeheader. For backward compatibility reasons, if maxheaderlen isnot specified it defaults to 0, so you must override it explicitlyif you want a different maxheaderlen. 'policy' is passed to theGenerator instance used to serialize the mesasge; if it is notspecified the policy associated with the message instance is used.If the message object contains binary data that is not encodedaccording to RFC standards, the non-compliant data will be replaced byunicode "unknown character" code points."""from email.generator import Generatorpolicy = self.policy if policy is None else policyfp = StringIO()g = Generator(fp,mangle_from_=False,maxheaderlen=maxheaderlen,policy=policy)g.flatten(self, unixfrom=unixfrom)return fp.getvalue()def __bytes__(self):"""Return the entire formatted message as a bytes object."""return self.as_bytes()def as_bytes(self, unixfrom=False, policy=None):"""Return the entire formatted message as a bytes object.Optional 'unixfrom', when true, means include the Unix From_ envelopeheader. 'policy' is passed to the BytesGenerator instance used toserialize the message; if not specified the policy associated withthe message instance is used."""from email.generator import BytesGeneratorpolicy = self.policy if policy is None else policyfp = BytesIO()g = BytesGenerator(fp, mangle_from_=False, policy=policy)g.flatten(self, unixfrom=unixfrom)return fp.getvalue()def is_multipart(self):"""Return True if the message consists of multiple parts."""return isinstance(self._payload, list)## Unix From_ line#def set_unixfrom(self, unixfrom):self._unixfrom = unixfromdef get_unixfrom(self):return self._unixfrom## Payload manipulation.#def attach(self, payload):"""Add the given payload to the current payload.The current payload will always be a list of objects after this methodis called. If you want to set the payload to a scalar object, useset_payload() instead."""if self._payload is None:self._payload = [payload]else:try:self._payload.append(payload)except AttributeError:raise TypeError("Attach is not valid on a message with a"" non-multipart payload")def get_payload(self, i=None, decode=False):"""Return a reference to the payload.The payload will either be a list object or a string. If you mutatethe list object, you modify the message's payload in place. Optionali returns that index into the payload.Optional decode is a flag indicating whether the payload should bedecoded or not, according to the Content-Transfer-Encoding header(default is False).When True and the message is not a multipart, the payload will bedecoded if this header's value is `quoted-printable' or `base64'. Ifsome other encoding is used, or the header is missing, or if thepayload has bogus data (i.e. bogus base64 or uuencoded data), thepayload is returned as-is.If the message is a multipart and the decode flag is True, then Noneis returned."""# Here is the logic table for this code, based on the email5.0.0 code:# i decode is_multipart result# ------ ------ ------------ ------------------------------# None True True None# i True True None# None False True _payload (a list)# i False True _payload element i (a Message)# i False False error (not a list)# i True False error (not a list)# None False False _payload# None True False _payload decoded (bytes)# Note that Barry planned to factor out the 'decode' case, but that# isn't so easy now that we handle the 8 bit data, which needs to be# converted in both the decode and non-decode path.if self.is_multipart():if decode:return Noneif i is None:return self._payloadelse:return self._payload[i]# For backward compatibility, Use isinstance and this error message# instead of the more logical is_multipart test.if i is not None and not isinstance(self._payload, list):raise TypeError('Expected list, got %s' % type(self._payload))payload = self._payload# cte might be a Header, so for now stringify it.cte = str(self.get('content-transfer-encoding', '')).lower()# payload may be bytes here.if isinstance(payload, str):if utils._has_surrogates(payload):bpayload = payload.encode('ascii', 'surrogateescape')if not decode:try:payload = bpayload.decode(self.get_param('charset', 'ascii'), 'replace')except LookupError:payload = bpayload.decode('ascii', 'replace')elif decode:try:bpayload = payload.encode('ascii')except UnicodeError:# This won't happen for RFC compliant messages (messages# containing only ASCII code points in the unicode input).# If it does happen, turn the string into bytes in a way# guaranteed not to fail.bpayload = payload.encode('raw-unicode-escape')if not decode:return payloadif cte == 'quoted-printable':return quopri.decodestring(bpayload)elif cte == 'base64':# XXX: this is a bit of a hack; decode_b should probably be factored# out somewhere, but I haven't figured out where yet.value, defects = decode_b(b''.join(bpayload.splitlines()))for defect in defects:self.policy.handle_defect(self, defect)return valueelif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):in_file = BytesIO(bpayload)out_file = BytesIO()try:uu.decode(in_file, out_file, quiet=True)return out_file.getvalue()except uu.Error:# Some decoding problemreturn bpayloadif isinstance(payload, str):return bpayloadreturn payloaddef set_payload(self, payload, charset=None):"""Set the payload to the given value.Optional charset sets the message's default character set. Seeset_charset() for details."""if hasattr(payload, 'encode'):if charset is None:self._payload = payloadreturnif not isinstance(charset, Charset):charset = Charset(charset)payload = payload.encode(charset.output_charset)if hasattr(payload, 'decode'):self._payload = payload.decode('ascii', 'surrogateescape')else:self._payload = payloadif charset is not None:self.set_charset(charset)def set_charset(self, charset):"""Set the charset of the payload to a given character set.charset can be a Charset instance, a string naming a character set, orNone. If it is a string it will be converted to a Charset instance.If charset is None, the charset parameter will be removed from theContent-Type field. Anything else will generate a TypeError.The message will be assumed to be of type text/* encoded withcharset.input_charset. It will be converted to charset.output_charsetand encoded properly, if needed, when generating the plain textrepresentation of the message. MIME headers (MIME-Version,Content-Type, Content-Transfer-Encoding) will be added as needed."""if charset is None:self.del_param('charset')self._charset = Nonereturnif not isinstance(charset, Charset):charset = Charset(charset)self._charset = charsetif 'MIME-Version' not in self:self.add_header('MIME-Version', '1.0')if 'Content-Type' not in self:self.add_header('Content-Type', 'text/plain',charset=charset.get_output_charset())else:self.set_param('charset', charset.get_output_charset())if charset != charset.get_output_charset():self._payload = charset.body_encode(self._payload)if 'Content-Transfer-Encoding' not in self:cte = charset.get_body_encoding()try:cte(self)except TypeError:# This 'if' is for backward compatibility, it allows unicode# through even though that won't work correctly if the# message is serialized.payload = self._payloadif payload:try:payload = payload.encode('ascii', 'surrogateescape')except UnicodeError:payload = payload.encode(charset.output_charset)self._payload = charset.body_encode(payload)self.add_header('Content-Transfer-Encoding', cte)def get_charset(self):"""Return the Charset instance associated with the message's payload."""return self._charset## MAPPING INTERFACE (partial)#def __len__(self):"""Return the total number of headers, including duplicates."""return len(self._headers)def __getitem__(self, name):"""Get a header value.Return None if the header is missing instead of raising an exception.Note that if the header appeared multiple times, exactly whichoccurrence gets returned is undefined. Use get_all() to get allthe values matching a header field name."""return self.get(name)def __setitem__(self, name, val):"""Set the value of a header.Note: this does not overwrite an existing header with the same fieldname. Use __delitem__() first to delete any existing headers."""max_count = self.policy.header_max_count(name)if max_count:lname = name.lower()found = 0for k, v in self._headers:if k.lower() == lname:found += 1if found >= max_count:raise ValueError("There may be at most {} {} headers ""in a message".format(max_count, name))self._headers.append(self.policy.header_store_parse(name, val))def __delitem__(self, name):"""Delete all occurrences of a header, if present.Does not raise an exception if the header is missing."""name = name.lower()newheaders = []for k, v in self._headers:if k.lower() != name:newheaders.append((k, v))self._headers = newheadersdef __contains__(self, name):return name.lower() in [k.lower() for k, v in self._headers]def __iter__(self):for field, value in self._headers:yield fielddef keys(self):"""Return a list of all the message's header field names.These will be sorted in the order they appeared in the originalmessage, or were added to the message, and may contain duplicates.Any fields deleted and re-inserted are always appended to the headerlist."""return [k for k, v in self._headers]def values(self):"""Return a list of all the message's header values.These will be sorted in the order they appeared in the originalmessage, or were added to the message, and may contain duplicates.Any fields deleted and re-inserted are always appended to the headerlist."""return [self.policy.header_fetch_parse(k, v)for k, v in self._headers]def items(self):"""Get all the message's header fields and values.These will be sorted in the order they appeared in the originalmessage, or were added to the message, and may contain duplicates.Any fields deleted and re-inserted are always appended to the headerlist."""return [(k, self.policy.header_fetch_parse(k, v))for k, v in self._headers]def get(self, name, failobj=None):"""Get a header value.Like __getitem__() but return failobj instead of None when the fieldis missing."""name = name.lower()for k, v in self._headers:if k.lower() == name:return self.policy.header_fetch_parse(k, v)return failobj## "Internal" methods (public API, but only intended for use by a parser# or generator, not normal application code.#def set_raw(self, name, value):"""Store name and value in the model without modification.This is an "internal" API, intended only for use by a parser."""self._headers.append((name, value))def raw_items(self):"""Return the (name, value) header pairs without modification.This is an "internal" API, intended only for use by a generator."""return iter(self._headers.copy())## Additional useful stuff#def get_all(self, name, failobj=None):"""Return a list of all the values for the named field.These will be sorted in the order they appeared in the originalmessage, and may contain duplicates. Any fields deleted andre-inserted are always appended to the header list.If no such fields exist, failobj is returned (defaults to None)."""values = []name = name.lower()for k, v in self._headers:if k.lower() == name:values.append(self.policy.header_fetch_parse(k, v))if not values:return failobjreturn valuesdef add_header(self, _name, _value, **_params):"""Extended header setting.name is the header field to add. keyword arguments can be used to setadditional parameters for the header field, with underscores convertedto dashes. Normally the parameter will be added as key="value" unlessvalue is None, in which case only the key will be added. If aparameter value contains non-ASCII characters it can be specified as athree-tuple of (charset, language, value), in which case it will beencoded according to RFC2231 rules. Otherwise it will be encoded usingthe utf-8 charset and a language of ''.Examples:msg.add_header('content-disposition', 'attachment', filename='bud.gif')msg.add_header('content-disposition', 'attachment',filename=('utf-8', '', Fußballer.ppt'))msg.add_header('content-disposition', 'attachment',filename='Fußballer.ppt'))"""parts = []for k, v in _params.items():if v is None:parts.append(k.replace('_', '-'))else:parts.append(_formatparam(k.replace('_', '-'), v))if _value is not None:parts.insert(0, _value)self[_name] = SEMISPACE.join(parts)def replace_header(self, _name, _value):"""Replace a header.Replace the first matching header found in the message, retainingheader order and case. If no matching header was found, a KeyError israised."""_name = _name.lower()for i, (k, v) in zip(range(len(self._headers)), self._headers):if k.lower() == _name:self._headers[i] = self.policy.header_store_parse(k, _value)breakelse:raise KeyError(_name)## Use these three methods instead of the three above.#def get_content_type(self):"""Return the message's content type.The returned string is coerced to lower case of the form`maintype/subtype'. If there was no Content-Type header in themessage, the default type as given by get_default_type() will bereturned. Since according to RFC 2045, messages always have a defaulttype this will always return a value.RFC 2045 defines a message's default type to be text/plain unless itappears inside a multipart/digest container, in which case it would bemessage/rfc822."""missing = object()value = self.get('content-type', missing)if value is missing:# This should have no parametersreturn self.get_default_type()ctype = _splitparam(value)[0].lower()# RFC 2045, section 5.2 says if its invalid, use text/plainif ctype.count('/') != 1:return 'text/plain'return ctypedef get_content_maintype(self):"""Return the message's main content type.This is the `maintype' part of the string returned byget_content_type()."""ctype = self.get_content_type()return ctype.split('/')[0]def get_content_subtype(self):"""Returns the message's sub-content type.This is the `subtype' part of the string returned byget_content_type()."""ctype = self.get_content_type()return ctype.split('/')[1]def get_default_type(self):"""Return the `default' content type.Most messages have a default content type of text/plain, except formessages that are subparts of multipart/digest containers. Suchsubparts have a default content type of message/rfc822."""return self._default_typedef set_default_type(self, ctype):"""Set the `default' content type.ctype should be either "text/plain" or "message/rfc822", although thisis not enforced. The default content type is not stored in theContent-Type header."""self._default_type = ctypedef _get_params_preserve(self, failobj, header):# Like get_params() but preserves the quoting of values. BAW:# should this be part of the public interface?missing = object()value = self.get(header, missing)if value is missing:return failobjparams = []for p in _parseparam(value):try:name, val = p.split('=', 1)name = name.strip()val = val.strip()except ValueError:# Must have been a bare attributename = p.strip()val = ''params.append((name, val))params = utils.decode_params(params)return paramsdef get_params(self, failobj=None, header='content-type', unquote=True):"""Return the message's Content-Type parameters, as a list.The elements of the returned list are 2-tuples of key/value pairs, assplit on the `=' sign. The left hand side of the `=' is the key,while the right hand side is the value. If there is no `=' sign inthe parameter the value is the empty string. The value is asdescribed in the get_param() method.Optional failobj is the object to return if there is no Content-Typeheader. Optional header is the header to search instead ofContent-Type. If unquote is True, the value is unquoted."""missing = object()params = self._get_params_preserve(missing, header)if params is missing:return failobjif unquote:return [(k, _unquotevalue(v)) for k, v in params]else:return paramsdef get_param(self, param, failobj=None, header='content-type',unquote=True):"""Return the parameter value if found in the Content-Type header.Optional failobj is the object to return if there is no Content-Typeheader, or the Content-Type header has no such parameter. Optionalheader is the header to search instead of Content-Type.Parameter keys are always compared case insensitively. The returnvalue can either be a string, or a 3-tuple if the parameter was RFC2231 encoded. When it's a 3-tuple, the elements of the value are ofthe form (CHARSET, LANGUAGE, VALUE). Note that both CHARSET andLANGUAGE can be None, in which case you should consider VALUE to beencoded in the us-ascii charset. You can usually ignore LANGUAGE.The parameter value (either the returned string, or the VALUE item inthe 3-tuple) is always unquoted, unless unquote is set to False.If your application doesn't care whether the parameter was RFC 2231encoded, it can turn the return value into a string as follows:rawparam = msg.get_param('foo')param = email.utils.collapse_rfc2231_value(rawparam)"""if header not in self:return failobjfor k, v in self._get_params_preserve(failobj, header):if k.lower() == param.lower():if unquote:return _unquotevalue(v)else:return vreturn failobjdef set_param(self, param, value, header='Content-Type', requote=True,charset=None, language='', replace=False):"""Set a parameter in the Content-Type header.If the parameter already exists in the header, its value will bereplaced with the new value.If header is Content-Type and has not yet been defined for thismessage, it will be set to "text/plain" and the new parameter andvalue will be appended as per RFC 2045.An alternate header can be specified in the header argument, and allparameters will be quoted as necessary unless requote is False.If charset is specified, the parameter will be encoded according to RFC2231. Optional language specifies the RFC 2231 language, defaultingto the empty string. Both charset and language should be strings."""if not isinstance(value, tuple) and charset:value = (charset, language, value)if header not in self and header.lower() == 'content-type':ctype = 'text/plain'else:ctype = self.get(header)if not self.get_param(param, header=header):if not ctype:ctype = _formatparam(param, value, requote)else:ctype = SEMISPACE.join([ctype, _formatparam(param, value, requote)])else:ctype = ''for old_param, old_value in self.get_params(header=header,unquote=requote):append_param = ''if old_param.lower() == param.lower():append_param = _formatparam(param, value, requote)else:append_param = _formatparam(old_param, old_value, requote)if not ctype:ctype = append_paramelse:ctype = SEMISPACE.join([ctype, append_param])if ctype != self.get(header):if replace:self.replace_header(header, ctype)else:del self[header]self[header] = ctypedef del_param(self, param, header='content-type', requote=True):"""Remove the given parameter completely from the Content-Type header.The header will be re-written in place without the parameter or itsvalue. All values will be quoted as necessary unless requote isFalse. Optional header specifies an alternative to the Content-Typeheader."""if header not in self:returnnew_ctype = ''for p, v in self.get_params(header=header, unquote=requote):if p.lower() != param.lower():if not new_ctype:new_ctype = _formatparam(p, v, requote)else:new_ctype = SEMISPACE.join([new_ctype,_formatparam(p, v, requote)])if new_ctype != self.get(header):del self[header]self[header] = new_ctypedef set_type(self, type, header='Content-Type', requote=True):"""Set the main type and subtype for the Content-Type header.type must be a string in the form "maintype/subtype", otherwise aValueError is raised.This method replaces the Content-Type header, keeping all theparameters in place. If requote is False, this leaves the existingheader's quoting as is. Otherwise, the parameters will be quoted (thedefault).An alternative header can be specified in the header argument. Whenthe Content-Type header is set, we'll always also add a MIME-Versionheader."""# BAW: should we be strict?if not type.count('/') == 1:raise ValueError# Set the Content-Type, you get a MIME-Versionif header.lower() == 'content-type':del self['mime-version']self['MIME-Version'] = '1.0'if header not in self:self[header] = typereturnparams = self.get_params(header=header, unquote=requote)del self[header]self[header] = type# Skip the first param; it's the old type.for p, v in params[1:]:self.set_param(p, v, header, requote)def get_filename(self, failobj=None):"""Return the filename associated with the payload if present.The filename is extracted from the Content-Disposition header's`filename' parameter, and it is unquoted. If that header is missingthe `filename' parameter, this method falls back to looking for the`name' parameter."""missing = object()filename = self.get_param('filename', missing, 'content-disposition')if filename is missing:filename = self.get_param('name', missing, 'content-type')if filename is missing:return failobjreturn utils.collapse_rfc2231_value(filename).strip()def get_boundary(self, failobj=None):"""Return the boundary associated with the payload if present.The boundary is extracted from the Content-Type header's `boundary'parameter, and it is unquoted."""missing = object()boundary = self.get_param('boundary', missing)if boundary is missing:return failobj# RFC 2046 says that boundaries may begin but not end in w/sreturn utils.collapse_rfc2231_value(boundary).rstrip()def set_boundary(self, boundary):"""Set the boundary parameter in Content-Type to 'boundary'.This is subtly different than deleting the Content-Type header andadding a new one with a new boundary parameter via add_header(). Themain difference is that using the set_boundary() method preserves theorder of the Content-Type header in the original message.HeaderParseError is raised if the message has no Content-Type header."""missing = object()params = self._get_params_preserve(missing, 'content-type')if params is missing:# There was no Content-Type header, and we don't know what type# to set it to, so raise an exception.raise errors.HeaderParseError('No Content-Type header found')newparams = []foundp = Falsefor pk, pv in params:if pk.lower() == 'boundary':newparams.append(('boundary', '"%s"' % boundary))foundp = Trueelse:newparams.append((pk, pv))if not foundp:# The original Content-Type header had no boundary attribute.# Tack one on the end. BAW: should we raise an exception# instead???newparams.append(('boundary', '"%s"' % boundary))# Replace the existing Content-Type header with the new valuenewheaders = []for h, v in self._headers:if h.lower() == 'content-type':parts = []for k, v in newparams:if v == '':parts.append(k)else:parts.append('%s=%s' % (k, v))val = SEMISPACE.join(parts)newheaders.append(self.policy.header_store_parse(h, val))else:newheaders.append((h, v))self._headers = newheadersdef get_content_charset(self, failobj=None):"""Return the charset parameter of the Content-Type header.The returned string is always coerced to lower case. If there is noContent-Type header, or if that header has no charset parameter,failobj is returned."""missing = object()charset = self.get_param('charset', missing)if charset is missing:return failobjif isinstance(charset, tuple):# RFC 2231 encoded, so decode it, and it better end up as ascii.pcharset = charset[0] or 'us-ascii'try:# LookupError will be raised if the charset isn't known to# Python. UnicodeError will be raised if the encoded text# contains a character not in the charset.as_bytes = charset[2].encode('raw-unicode-escape')charset = str(as_bytes, pcharset)except (LookupError, UnicodeError):charset = charset[2]# charset characters must be in us-ascii rangetry:charset.encode('us-ascii')except UnicodeError:return failobj# RFC 2046, 4ドル.1.2 says charsets are not case sensitivereturn charset.lower()def get_charsets(self, failobj=None):"""Return a list containing the charset(s) used in this message.The returned list of items describes the Content-Type headers'charset parameter for this message and all the subparts in itspayload.Each item will either be a string (the value of the charset parameterin the Content-Type header of that part) or the value of the'failobj' parameter (defaults to None), if the part does not have amain MIME type of "text", or the charset is not defined.The list will contain one string for each part of the message, plusone for the container message (i.e. self), so that a non-multipartmessage will still return a list of length 1."""return [part.get_content_charset(failobj) for part in self.walk()]def get_content_disposition(self):"""Return the message's content-disposition if it exists, or None.The return values can be either 'inline', 'attachment' or Noneaccording to the rfc2183."""value = self.get('content-disposition')if value is None:return Nonec_d = _splitparam(value)[0].lower()return c_d# I.e. def walk(self): ...from email.iterators import walkclass MIMEPart(Message):def __init__(self, policy=None):if policy is None:from email.policy import defaultpolicy = defaultMessage.__init__(self, policy)def as_string(self, unixfrom=False, maxheaderlen=None, policy=None):"""Return the entire formatted message as a string.Optional 'unixfrom', when true, means include the Unix From_ envelopeheader. maxheaderlen is retained for backward compatibility with thebase Message class, but defaults to None, meaning that the policy valuefor max_line_length controls the header maximum length. 'policy' ispassed to the Generator instance used to serialize the mesasge; if itis not specified the policy associated with the message instance isused."""policy = self.policy if policy is None else policyif maxheaderlen is None:maxheaderlen = policy.max_line_lengthreturn super().as_string(maxheaderlen=maxheaderlen, policy=policy)def __str__(self):return self.as_string(policy=self.policy.clone(utf8=True))def is_attachment(self):c_d = self.get('content-disposition')return False if c_d is None else c_d.content_disposition == 'attachment'def _find_body(self, part, preferencelist):if part.is_attachment():returnmaintype, subtype = part.get_content_type().split('/')if maintype == 'text':if subtype in preferencelist:yield (preferencelist.index(subtype), part)returnif maintype != 'multipart':returnif subtype != 'related':for subpart in part.iter_parts():yield from self._find_body(subpart, preferencelist)returnif 'related' in preferencelist:yield (preferencelist.index('related'), part)candidate = Nonestart = part.get_param('start')if start:for subpart in part.iter_parts():if subpart['content-id'] == start:candidate = subpartbreakif candidate is None:subparts = part.get_payload()candidate = subparts[0] if subparts else Noneif candidate is not None:yield from self._find_body(candidate, preferencelist)def get_body(self, preferencelist=('related', 'html', 'plain')):"""Return best candidate mime part for display as 'body' of message.Do a depth first search, starting with self, looking for the first partmatching each of the items in preferencelist, and return the partcorresponding to the first item that has a match, or None if no itemshave a match. If 'related' is not included in preferencelist, considerthe root part of any multipart/related encountered as a candidatematch. Ignore parts with 'Content-Disposition: attachment'."""best_prio = len(preferencelist)body = Nonefor prio, part in self._find_body(self, preferencelist):if prio < best_prio:best_prio = priobody = partif prio == 0:breakreturn body_body_types = {('text', 'plain'),('text', 'html'),('multipart', 'related'),('multipart', 'alternative')}def iter_attachments(self):"""Return an iterator over the non-main parts of a multipart.Skip the first of each occurrence of text/plain, text/html,multipart/related, or multipart/alternative in the multipart (unlessthey have a 'Content-Disposition: attachment' header) and include allremaining subparts in the returned iterator. When applied to amultipart/related, return all parts except the root part. Return anempty iterator when applied to a multipart/alternative or anon-multipart."""maintype, subtype = self.get_content_type().split('/')if maintype != 'multipart' or subtype == 'alternative':returnparts = self.get_payload().copy()if maintype == 'multipart' and subtype == 'related':# For related, we treat everything but the root as an attachment.# The root may be indicated by 'start'; if there's no start or we# can't find the named start, treat the first subpart as the root.start = self.get_param('start')if start:found = Falseattachments = []for part in parts:if part.get('content-id') == start:found = Trueelse:attachments.append(part)if found:yield from attachmentsreturnparts.pop(0)yield from partsreturn# Otherwise we more or less invert the remaining logic in get_body.# This only really works in edge cases (ex: non-text related or# alternatives) if the sending agent sets content-disposition.seen = [] # Only skip the first example of each candidate type.for part in parts:maintype, subtype = part.get_content_type().split('/')if ((maintype, subtype) in self._body_types andnot part.is_attachment() and subtype not in seen):seen.append(subtype)continueyield partdef iter_parts(self):"""Return an iterator over all immediate subparts of a multipart.Return an empty iterator for a non-multipart."""if self.get_content_maintype() == 'multipart':yield from self.get_payload()def get_content(self, *args, content_manager=None, **kw):if content_manager is None:content_manager = self.policy.content_managerreturn content_manager.get_content(self, *args, **kw)def set_content(self, *args, content_manager=None, **kw):if content_manager is None:content_manager = self.policy.content_managercontent_manager.set_content(self, *args, **kw)def _make_multipart(self, subtype, disallowed_subtypes, boundary):if self.get_content_maintype() == 'multipart':existing_subtype = self.get_content_subtype()disallowed_subtypes = disallowed_subtypes + (subtype,)if existing_subtype in disallowed_subtypes:raise ValueError("Cannot convert {} to {}".format(existing_subtype, subtype))keep_headers = []part_headers = []for name, value in self._headers:if name.lower().startswith('content-'):part_headers.append((name, value))else:keep_headers.append((name, value))if part_headers:# There is existing content, move it to the first subpart.part = type(self)(policy=self.policy)part._headers = part_headerspart._payload = self._payloadself._payload = [part]else:self._payload = []self._headers = keep_headersself['Content-Type'] = 'multipart/' + subtypeif boundary is not None:self.set_param('boundary', boundary)def make_related(self, boundary=None):self._make_multipart('related', ('alternative', 'mixed'), boundary)def make_alternative(self, boundary=None):self._make_multipart('alternative', ('mixed',), boundary)def make_mixed(self, boundary=None):self._make_multipart('mixed', (), boundary)def _add_multipart(self, _subtype, *args, _disp=None, **kw):if (self.get_content_maintype() != 'multipart' orself.get_content_subtype() != _subtype):getattr(self, 'make_' + _subtype)()part = type(self)(policy=self.policy)part.set_content(*args, **kw)if _disp and 'content-disposition' not in part:part['Content-Disposition'] = _dispself.attach(part)def add_related(self, *args, **kw):self._add_multipart('related', *args, _disp='inline', **kw)def add_alternative(self, *args, **kw):self._add_multipart('alternative', *args, **kw)def add_attachment(self, *args, **kw):self._add_multipart('mixed', *args, _disp='attachment', **kw)def clear(self):self._headers = []self._payload = Nonedef clear_content(self):self._headers = [(n, v) for n, v in self._headersif not n.lower().startswith('content-')]self._payload = Noneclass EmailMessage(MIMEPart):def set_content(self, *args, **kw):super().set_content(*args, **kw)if 'MIME-Version' not in self:self['MIME-Version'] = '1.0'
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。