annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/email/message.py @ 69:33d812a61356

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 17:55:14 -0400
parents
children
rev   line source
jpayne@69 1 # Copyright (C) 2001-2007 Python Software Foundation
jpayne@69 2 # Author: Barry Warsaw
jpayne@69 3 # Contact: email-sig@python.org
jpayne@69 4
jpayne@69 5 """Basic message object for the email package object model."""
jpayne@69 6
jpayne@69 7 __all__ = ['Message', 'EmailMessage']
jpayne@69 8
jpayne@69 9 import re
jpayne@69 10 import uu
jpayne@69 11 import quopri
jpayne@69 12 from io import BytesIO, StringIO
jpayne@69 13
jpayne@69 14 # Intrapackage imports
jpayne@69 15 from email import utils
jpayne@69 16 from email import errors
jpayne@69 17 from email._policybase import Policy, compat32
jpayne@69 18 from email import charset as _charset
jpayne@69 19 from email._encoded_words import decode_b
jpayne@69 20 Charset = _charset.Charset
jpayne@69 21
jpayne@69 22 SEMISPACE = '; '
jpayne@69 23
jpayne@69 24 # Regular expression that matches `special' characters in parameters, the
jpayne@69 25 # existence of which force quoting of the parameter value.
jpayne@69 26 tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
jpayne@69 27
jpayne@69 28
jpayne@69 29 def _splitparam(param):
jpayne@69 30 # Split header parameters. BAW: this may be too simple. It isn't
jpayne@69 31 # strictly RFC 2045 (section 5.1) compliant, but it catches most headers
jpayne@69 32 # found in the wild. We may eventually need a full fledged parser.
jpayne@69 33 # RDM: we might have a Header here; for now just stringify it.
jpayne@69 34 a, sep, b = str(param).partition(';')
jpayne@69 35 if not sep:
jpayne@69 36 return a.strip(), None
jpayne@69 37 return a.strip(), b.strip()
jpayne@69 38
jpayne@69 39 def _formatparam(param, value=None, quote=True):
jpayne@69 40 """Convenience function to format and return a key=value pair.
jpayne@69 41
jpayne@69 42 This will quote the value if needed or if quote is true. If value is a
jpayne@69 43 three tuple (charset, language, value), it will be encoded according
jpayne@69 44 to RFC2231 rules. If it contains non-ascii characters it will likewise
jpayne@69 45 be encoded according to RFC2231 rules, using the utf-8 charset and
jpayne@69 46 a null language.
jpayne@69 47 """
jpayne@69 48 if value is not None and len(value) > 0:
jpayne@69 49 # A tuple is used for RFC 2231 encoded parameter values where items
jpayne@69 50 # are (charset, language, value). charset is a string, not a Charset
jpayne@69 51 # instance. RFC 2231 encoded values are never quoted, per RFC.
jpayne@69 52 if isinstance(value, tuple):
jpayne@69 53 # Encode as per RFC 2231
jpayne@69 54 param += '*'
jpayne@69 55 value = utils.encode_rfc2231(value[2], value[0], value[1])
jpayne@69 56 return '%s=%s' % (param, value)
jpayne@69 57 else:
jpayne@69 58 try:
jpayne@69 59 value.encode('ascii')
jpayne@69 60 except UnicodeEncodeError:
jpayne@69 61 param += '*'
jpayne@69 62 value = utils.encode_rfc2231(value, 'utf-8', '')
jpayne@69 63 return '%s=%s' % (param, value)
jpayne@69 64 # BAW: Please check this. I think that if quote is set it should
jpayne@69 65 # force quoting even if not necessary.
jpayne@69 66 if quote or tspecials.search(value):
jpayne@69 67 return '%s="%s"' % (param, utils.quote(value))
jpayne@69 68 else:
jpayne@69 69 return '%s=%s' % (param, value)
jpayne@69 70 else:
jpayne@69 71 return param
jpayne@69 72
jpayne@69 73 def _parseparam(s):
jpayne@69 74 # RDM This might be a Header, so for now stringify it.
jpayne@69 75 s = ';' + str(s)
jpayne@69 76 plist = []
jpayne@69 77 while s[:1] == ';':
jpayne@69 78 s = s[1:]
jpayne@69 79 end = s.find(';')
jpayne@69 80 while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2:
jpayne@69 81 end = s.find(';', end + 1)
jpayne@69 82 if end < 0:
jpayne@69 83 end = len(s)
jpayne@69 84 f = s[:end]
jpayne@69 85 if '=' in f:
jpayne@69 86 i = f.index('=')
jpayne@69 87 f = f[:i].strip().lower() + '=' + f[i+1:].strip()
jpayne@69 88 plist.append(f.strip())
jpayne@69 89 s = s[end:]
jpayne@69 90 return plist
jpayne@69 91
jpayne@69 92
jpayne@69 93 def _unquotevalue(value):
jpayne@69 94 # This is different than utils.collapse_rfc2231_value() because it doesn't
jpayne@69 95 # try to convert the value to a unicode. Message.get_param() and
jpayne@69 96 # Message.get_params() are both currently defined to return the tuple in
jpayne@69 97 # the face of RFC 2231 parameters.
jpayne@69 98 if isinstance(value, tuple):
jpayne@69 99 return value[0], value[1], utils.unquote(value[2])
jpayne@69 100 else:
jpayne@69 101 return utils.unquote(value)
jpayne@69 102
jpayne@69 103
jpayne@69 104
jpayne@69 105 class Message:
jpayne@69 106 """Basic message object.
jpayne@69 107
jpayne@69 108 A message object is defined as something that has a bunch of RFC 2822
jpayne@69 109 headers and a payload. It may optionally have an envelope header
jpayne@69 110 (a.k.a. Unix-From or From_ header). If the message is a container (i.e. a
jpayne@69 111 multipart or a message/rfc822), then the payload is a list of Message
jpayne@69 112 objects, otherwise it is a string.
jpayne@69 113
jpayne@69 114 Message objects implement part of the `mapping' interface, which assumes
jpayne@69 115 there is exactly one occurrence of the header per message. Some headers
jpayne@69 116 do in fact appear multiple times (e.g. Received) and for those headers,
jpayne@69 117 you must use the explicit API to set or get all the headers. Not all of
jpayne@69 118 the mapping methods are implemented.
jpayne@69 119 """
jpayne@69 120 def __init__(self, policy=compat32):
jpayne@69 121 self.policy = policy
jpayne@69 122 self._headers = []
jpayne@69 123 self._unixfrom = None
jpayne@69 124 self._payload = None
jpayne@69 125 self._charset = None
jpayne@69 126 # Defaults for multipart messages
jpayne@69 127 self.preamble = self.epilogue = None
jpayne@69 128 self.defects = []
jpayne@69 129 # Default content type
jpayne@69 130 self._default_type = 'text/plain'
jpayne@69 131
jpayne@69 132 def __str__(self):
jpayne@69 133 """Return the entire formatted message as a string.
jpayne@69 134 """
jpayne@69 135 return self.as_string()
jpayne@69 136
jpayne@69 137 def as_string(self, unixfrom=False, maxheaderlen=0, policy=None):
jpayne@69 138 """Return the entire formatted message as a string.
jpayne@69 139
jpayne@69 140 Optional 'unixfrom', when true, means include the Unix From_ envelope
jpayne@69 141 header. For backward compatibility reasons, if maxheaderlen is
jpayne@69 142 not specified it defaults to 0, so you must override it explicitly
jpayne@69 143 if you want a different maxheaderlen. 'policy' is passed to the
jpayne@69 144 Generator instance used to serialize the mesasge; if it is not
jpayne@69 145 specified the policy associated with the message instance is used.
jpayne@69 146
jpayne@69 147 If the message object contains binary data that is not encoded
jpayne@69 148 according to RFC standards, the non-compliant data will be replaced by
jpayne@69 149 unicode "unknown character" code points.
jpayne@69 150 """
jpayne@69 151 from email.generator import Generator
jpayne@69 152 policy = self.policy if policy is None else policy
jpayne@69 153 fp = StringIO()
jpayne@69 154 g = Generator(fp,
jpayne@69 155 mangle_from_=False,
jpayne@69 156 maxheaderlen=maxheaderlen,
jpayne@69 157 policy=policy)
jpayne@69 158 g.flatten(self, unixfrom=unixfrom)
jpayne@69 159 return fp.getvalue()
jpayne@69 160
jpayne@69 161 def __bytes__(self):
jpayne@69 162 """Return the entire formatted message as a bytes object.
jpayne@69 163 """
jpayne@69 164 return self.as_bytes()
jpayne@69 165
jpayne@69 166 def as_bytes(self, unixfrom=False, policy=None):
jpayne@69 167 """Return the entire formatted message as a bytes object.
jpayne@69 168
jpayne@69 169 Optional 'unixfrom', when true, means include the Unix From_ envelope
jpayne@69 170 header. 'policy' is passed to the BytesGenerator instance used to
jpayne@69 171 serialize the message; if not specified the policy associated with
jpayne@69 172 the message instance is used.
jpayne@69 173 """
jpayne@69 174 from email.generator import BytesGenerator
jpayne@69 175 policy = self.policy if policy is None else policy
jpayne@69 176 fp = BytesIO()
jpayne@69 177 g = BytesGenerator(fp, mangle_from_=False, policy=policy)
jpayne@69 178 g.flatten(self, unixfrom=unixfrom)
jpayne@69 179 return fp.getvalue()
jpayne@69 180
jpayne@69 181 def is_multipart(self):
jpayne@69 182 """Return True if the message consists of multiple parts."""
jpayne@69 183 return isinstance(self._payload, list)
jpayne@69 184
jpayne@69 185 #
jpayne@69 186 # Unix From_ line
jpayne@69 187 #
jpayne@69 188 def set_unixfrom(self, unixfrom):
jpayne@69 189 self._unixfrom = unixfrom
jpayne@69 190
jpayne@69 191 def get_unixfrom(self):
jpayne@69 192 return self._unixfrom
jpayne@69 193
jpayne@69 194 #
jpayne@69 195 # Payload manipulation.
jpayne@69 196 #
jpayne@69 197 def attach(self, payload):
jpayne@69 198 """Add the given payload to the current payload.
jpayne@69 199
jpayne@69 200 The current payload will always be a list of objects after this method
jpayne@69 201 is called. If you want to set the payload to a scalar object, use
jpayne@69 202 set_payload() instead.
jpayne@69 203 """
jpayne@69 204 if self._payload is None:
jpayne@69 205 self._payload = [payload]
jpayne@69 206 else:
jpayne@69 207 try:
jpayne@69 208 self._payload.append(payload)
jpayne@69 209 except AttributeError:
jpayne@69 210 raise TypeError("Attach is not valid on a message with a"
jpayne@69 211 " non-multipart payload")
jpayne@69 212
jpayne@69 213 def get_payload(self, i=None, decode=False):
jpayne@69 214 """Return a reference to the payload.
jpayne@69 215
jpayne@69 216 The payload will either be a list object or a string. If you mutate
jpayne@69 217 the list object, you modify the message's payload in place. Optional
jpayne@69 218 i returns that index into the payload.
jpayne@69 219
jpayne@69 220 Optional decode is a flag indicating whether the payload should be
jpayne@69 221 decoded or not, according to the Content-Transfer-Encoding header
jpayne@69 222 (default is False).
jpayne@69 223
jpayne@69 224 When True and the message is not a multipart, the payload will be
jpayne@69 225 decoded if this header's value is `quoted-printable' or `base64'. If
jpayne@69 226 some other encoding is used, or the header is missing, or if the
jpayne@69 227 payload has bogus data (i.e. bogus base64 or uuencoded data), the
jpayne@69 228 payload is returned as-is.
jpayne@69 229
jpayne@69 230 If the message is a multipart and the decode flag is True, then None
jpayne@69 231 is returned.
jpayne@69 232 """
jpayne@69 233 # Here is the logic table for this code, based on the email5.0.0 code:
jpayne@69 234 # i decode is_multipart result
jpayne@69 235 # ------ ------ ------------ ------------------------------
jpayne@69 236 # None True True None
jpayne@69 237 # i True True None
jpayne@69 238 # None False True _payload (a list)
jpayne@69 239 # i False True _payload element i (a Message)
jpayne@69 240 # i False False error (not a list)
jpayne@69 241 # i True False error (not a list)
jpayne@69 242 # None False False _payload
jpayne@69 243 # None True False _payload decoded (bytes)
jpayne@69 244 # Note that Barry planned to factor out the 'decode' case, but that
jpayne@69 245 # isn't so easy now that we handle the 8 bit data, which needs to be
jpayne@69 246 # converted in both the decode and non-decode path.
jpayne@69 247 if self.is_multipart():
jpayne@69 248 if decode:
jpayne@69 249 return None
jpayne@69 250 if i is None:
jpayne@69 251 return self._payload
jpayne@69 252 else:
jpayne@69 253 return self._payload[i]
jpayne@69 254 # For backward compatibility, Use isinstance and this error message
jpayne@69 255 # instead of the more logical is_multipart test.
jpayne@69 256 if i is not None and not isinstance(self._payload, list):
jpayne@69 257 raise TypeError('Expected list, got %s' % type(self._payload))
jpayne@69 258 payload = self._payload
jpayne@69 259 # cte might be a Header, so for now stringify it.
jpayne@69 260 cte = str(self.get('content-transfer-encoding', '')).lower()
jpayne@69 261 # payload may be bytes here.
jpayne@69 262 if isinstance(payload, str):
jpayne@69 263 if utils._has_surrogates(payload):
jpayne@69 264 bpayload = payload.encode('ascii', 'surrogateescape')
jpayne@69 265 if not decode:
jpayne@69 266 try:
jpayne@69 267 payload = bpayload.decode(self.get_param('charset', 'ascii'), 'replace')
jpayne@69 268 except LookupError:
jpayne@69 269 payload = bpayload.decode('ascii', 'replace')
jpayne@69 270 elif decode:
jpayne@69 271 try:
jpayne@69 272 bpayload = payload.encode('ascii')
jpayne@69 273 except UnicodeError:
jpayne@69 274 # This won't happen for RFC compliant messages (messages
jpayne@69 275 # containing only ASCII code points in the unicode input).
jpayne@69 276 # If it does happen, turn the string into bytes in a way
jpayne@69 277 # guaranteed not to fail.
jpayne@69 278 bpayload = payload.encode('raw-unicode-escape')
jpayne@69 279 if not decode:
jpayne@69 280 return payload
jpayne@69 281 if cte == 'quoted-printable':
jpayne@69 282 return quopri.decodestring(bpayload)
jpayne@69 283 elif cte == 'base64':
jpayne@69 284 # XXX: this is a bit of a hack; decode_b should probably be factored
jpayne@69 285 # out somewhere, but I haven't figured out where yet.
jpayne@69 286 value, defects = decode_b(b''.join(bpayload.splitlines()))
jpayne@69 287 for defect in defects:
jpayne@69 288 self.policy.handle_defect(self, defect)
jpayne@69 289 return value
jpayne@69 290 elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
jpayne@69 291 in_file = BytesIO(bpayload)
jpayne@69 292 out_file = BytesIO()
jpayne@69 293 try:
jpayne@69 294 uu.decode(in_file, out_file, quiet=True)
jpayne@69 295 return out_file.getvalue()
jpayne@69 296 except uu.Error:
jpayne@69 297 # Some decoding problem
jpayne@69 298 return bpayload
jpayne@69 299 if isinstance(payload, str):
jpayne@69 300 return bpayload
jpayne@69 301 return payload
jpayne@69 302
jpayne@69 303 def set_payload(self, payload, charset=None):
jpayne@69 304 """Set the payload to the given value.
jpayne@69 305
jpayne@69 306 Optional charset sets the message's default character set. See
jpayne@69 307 set_charset() for details.
jpayne@69 308 """
jpayne@69 309 if hasattr(payload, 'encode'):
jpayne@69 310 if charset is None:
jpayne@69 311 self._payload = payload
jpayne@69 312 return
jpayne@69 313 if not isinstance(charset, Charset):
jpayne@69 314 charset = Charset(charset)
jpayne@69 315 payload = payload.encode(charset.output_charset)
jpayne@69 316 if hasattr(payload, 'decode'):
jpayne@69 317 self._payload = payload.decode('ascii', 'surrogateescape')
jpayne@69 318 else:
jpayne@69 319 self._payload = payload
jpayne@69 320 if charset is not None:
jpayne@69 321 self.set_charset(charset)
jpayne@69 322
jpayne@69 323 def set_charset(self, charset):
jpayne@69 324 """Set the charset of the payload to a given character set.
jpayne@69 325
jpayne@69 326 charset can be a Charset instance, a string naming a character set, or
jpayne@69 327 None. If it is a string it will be converted to a Charset instance.
jpayne@69 328 If charset is None, the charset parameter will be removed from the
jpayne@69 329 Content-Type field. Anything else will generate a TypeError.
jpayne@69 330
jpayne@69 331 The message will be assumed to be of type text/* encoded with
jpayne@69 332 charset.input_charset. It will be converted to charset.output_charset
jpayne@69 333 and encoded properly, if needed, when generating the plain text
jpayne@69 334 representation of the message. MIME headers (MIME-Version,
jpayne@69 335 Content-Type, Content-Transfer-Encoding) will be added as needed.
jpayne@69 336 """
jpayne@69 337 if charset is None:
jpayne@69 338 self.del_param('charset')
jpayne@69 339 self._charset = None
jpayne@69 340 return
jpayne@69 341 if not isinstance(charset, Charset):
jpayne@69 342 charset = Charset(charset)
jpayne@69 343 self._charset = charset
jpayne@69 344 if 'MIME-Version' not in self:
jpayne@69 345 self.add_header('MIME-Version', '1.0')
jpayne@69 346 if 'Content-Type' not in self:
jpayne@69 347 self.add_header('Content-Type', 'text/plain',
jpayne@69 348 charset=charset.get_output_charset())
jpayne@69 349 else:
jpayne@69 350 self.set_param('charset', charset.get_output_charset())
jpayne@69 351 if charset != charset.get_output_charset():
jpayne@69 352 self._payload = charset.body_encode(self._payload)
jpayne@69 353 if 'Content-Transfer-Encoding' not in self:
jpayne@69 354 cte = charset.get_body_encoding()
jpayne@69 355 try:
jpayne@69 356 cte(self)
jpayne@69 357 except TypeError:
jpayne@69 358 # This 'if' is for backward compatibility, it allows unicode
jpayne@69 359 # through even though that won't work correctly if the
jpayne@69 360 # message is serialized.
jpayne@69 361 payload = self._payload
jpayne@69 362 if payload:
jpayne@69 363 try:
jpayne@69 364 payload = payload.encode('ascii', 'surrogateescape')
jpayne@69 365 except UnicodeError:
jpayne@69 366 payload = payload.encode(charset.output_charset)
jpayne@69 367 self._payload = charset.body_encode(payload)
jpayne@69 368 self.add_header('Content-Transfer-Encoding', cte)
jpayne@69 369
jpayne@69 370 def get_charset(self):
jpayne@69 371 """Return the Charset instance associated with the message's payload.
jpayne@69 372 """
jpayne@69 373 return self._charset
jpayne@69 374
jpayne@69 375 #
jpayne@69 376 # MAPPING INTERFACE (partial)
jpayne@69 377 #
jpayne@69 378 def __len__(self):
jpayne@69 379 """Return the total number of headers, including duplicates."""
jpayne@69 380 return len(self._headers)
jpayne@69 381
jpayne@69 382 def __getitem__(self, name):
jpayne@69 383 """Get a header value.
jpayne@69 384
jpayne@69 385 Return None if the header is missing instead of raising an exception.
jpayne@69 386
jpayne@69 387 Note that if the header appeared multiple times, exactly which
jpayne@69 388 occurrence gets returned is undefined. Use get_all() to get all
jpayne@69 389 the values matching a header field name.
jpayne@69 390 """
jpayne@69 391 return self.get(name)
jpayne@69 392
jpayne@69 393 def __setitem__(self, name, val):
jpayne@69 394 """Set the value of a header.
jpayne@69 395
jpayne@69 396 Note: this does not overwrite an existing header with the same field
jpayne@69 397 name. Use __delitem__() first to delete any existing headers.
jpayne@69 398 """
jpayne@69 399 max_count = self.policy.header_max_count(name)
jpayne@69 400 if max_count:
jpayne@69 401 lname = name.lower()
jpayne@69 402 found = 0
jpayne@69 403 for k, v in self._headers:
jpayne@69 404 if k.lower() == lname:
jpayne@69 405 found += 1
jpayne@69 406 if found >= max_count:
jpayne@69 407 raise ValueError("There may be at most {} {} headers "
jpayne@69 408 "in a message".format(max_count, name))
jpayne@69 409 self._headers.append(self.policy.header_store_parse(name, val))
jpayne@69 410
jpayne@69 411 def __delitem__(self, name):
jpayne@69 412 """Delete all occurrences of a header, if present.
jpayne@69 413
jpayne@69 414 Does not raise an exception if the header is missing.
jpayne@69 415 """
jpayne@69 416 name = name.lower()
jpayne@69 417 newheaders = []
jpayne@69 418 for k, v in self._headers:
jpayne@69 419 if k.lower() != name:
jpayne@69 420 newheaders.append((k, v))
jpayne@69 421 self._headers = newheaders
jpayne@69 422
jpayne@69 423 def __contains__(self, name):
jpayne@69 424 return name.lower() in [k.lower() for k, v in self._headers]
jpayne@69 425
jpayne@69 426 def __iter__(self):
jpayne@69 427 for field, value in self._headers:
jpayne@69 428 yield field
jpayne@69 429
jpayne@69 430 def keys(self):
jpayne@69 431 """Return a list of all the message's header field names.
jpayne@69 432
jpayne@69 433 These will be sorted in the order they appeared in the original
jpayne@69 434 message, or were added to the message, and may contain duplicates.
jpayne@69 435 Any fields deleted and re-inserted are always appended to the header
jpayne@69 436 list.
jpayne@69 437 """
jpayne@69 438 return [k for k, v in self._headers]
jpayne@69 439
jpayne@69 440 def values(self):
jpayne@69 441 """Return a list of all the message's header values.
jpayne@69 442
jpayne@69 443 These will be sorted in the order they appeared in the original
jpayne@69 444 message, or were added to the message, and may contain duplicates.
jpayne@69 445 Any fields deleted and re-inserted are always appended to the header
jpayne@69 446 list.
jpayne@69 447 """
jpayne@69 448 return [self.policy.header_fetch_parse(k, v)
jpayne@69 449 for k, v in self._headers]
jpayne@69 450
jpayne@69 451 def items(self):
jpayne@69 452 """Get all the message's header fields and values.
jpayne@69 453
jpayne@69 454 These will be sorted in the order they appeared in the original
jpayne@69 455 message, or were added to the message, and may contain duplicates.
jpayne@69 456 Any fields deleted and re-inserted are always appended to the header
jpayne@69 457 list.
jpayne@69 458 """
jpayne@69 459 return [(k, self.policy.header_fetch_parse(k, v))
jpayne@69 460 for k, v in self._headers]
jpayne@69 461
jpayne@69 462 def get(self, name, failobj=None):
jpayne@69 463 """Get a header value.
jpayne@69 464
jpayne@69 465 Like __getitem__() but return failobj instead of None when the field
jpayne@69 466 is missing.
jpayne@69 467 """
jpayne@69 468 name = name.lower()
jpayne@69 469 for k, v in self._headers:
jpayne@69 470 if k.lower() == name:
jpayne@69 471 return self.policy.header_fetch_parse(k, v)
jpayne@69 472 return failobj
jpayne@69 473
jpayne@69 474 #
jpayne@69 475 # "Internal" methods (public API, but only intended for use by a parser
jpayne@69 476 # or generator, not normal application code.
jpayne@69 477 #
jpayne@69 478
jpayne@69 479 def set_raw(self, name, value):
jpayne@69 480 """Store name and value in the model without modification.
jpayne@69 481
jpayne@69 482 This is an "internal" API, intended only for use by a parser.
jpayne@69 483 """
jpayne@69 484 self._headers.append((name, value))
jpayne@69 485
jpayne@69 486 def raw_items(self):
jpayne@69 487 """Return the (name, value) header pairs without modification.
jpayne@69 488
jpayne@69 489 This is an "internal" API, intended only for use by a generator.
jpayne@69 490 """
jpayne@69 491 return iter(self._headers.copy())
jpayne@69 492
jpayne@69 493 #
jpayne@69 494 # Additional useful stuff
jpayne@69 495 #
jpayne@69 496
jpayne@69 497 def get_all(self, name, failobj=None):
jpayne@69 498 """Return a list of all the values for the named field.
jpayne@69 499
jpayne@69 500 These will be sorted in the order they appeared in the original
jpayne@69 501 message, and may contain duplicates. Any fields deleted and
jpayne@69 502 re-inserted are always appended to the header list.
jpayne@69 503
jpayne@69 504 If no such fields exist, failobj is returned (defaults to None).
jpayne@69 505 """
jpayne@69 506 values = []
jpayne@69 507 name = name.lower()
jpayne@69 508 for k, v in self._headers:
jpayne@69 509 if k.lower() == name:
jpayne@69 510 values.append(self.policy.header_fetch_parse(k, v))
jpayne@69 511 if not values:
jpayne@69 512 return failobj
jpayne@69 513 return values
jpayne@69 514
jpayne@69 515 def add_header(self, _name, _value, **_params):
jpayne@69 516 """Extended header setting.
jpayne@69 517
jpayne@69 518 name is the header field to add. keyword arguments can be used to set
jpayne@69 519 additional parameters for the header field, with underscores converted
jpayne@69 520 to dashes. Normally the parameter will be added as key="value" unless
jpayne@69 521 value is None, in which case only the key will be added. If a
jpayne@69 522 parameter value contains non-ASCII characters it can be specified as a
jpayne@69 523 three-tuple of (charset, language, value), in which case it will be
jpayne@69 524 encoded according to RFC2231 rules. Otherwise it will be encoded using
jpayne@69 525 the utf-8 charset and a language of ''.
jpayne@69 526
jpayne@69 527 Examples:
jpayne@69 528
jpayne@69 529 msg.add_header('content-disposition', 'attachment', filename='bud.gif')
jpayne@69 530 msg.add_header('content-disposition', 'attachment',
jpayne@69 531 filename=('utf-8', '', Fußballer.ppt'))
jpayne@69 532 msg.add_header('content-disposition', 'attachment',
jpayne@69 533 filename='Fußballer.ppt'))
jpayne@69 534 """
jpayne@69 535 parts = []
jpayne@69 536 for k, v in _params.items():
jpayne@69 537 if v is None:
jpayne@69 538 parts.append(k.replace('_', '-'))
jpayne@69 539 else:
jpayne@69 540 parts.append(_formatparam(k.replace('_', '-'), v))
jpayne@69 541 if _value is not None:
jpayne@69 542 parts.insert(0, _value)
jpayne@69 543 self[_name] = SEMISPACE.join(parts)
jpayne@69 544
jpayne@69 545 def replace_header(self, _name, _value):
jpayne@69 546 """Replace a header.
jpayne@69 547
jpayne@69 548 Replace the first matching header found in the message, retaining
jpayne@69 549 header order and case. If no matching header was found, a KeyError is
jpayne@69 550 raised.
jpayne@69 551 """
jpayne@69 552 _name = _name.lower()
jpayne@69 553 for i, (k, v) in zip(range(len(self._headers)), self._headers):
jpayne@69 554 if k.lower() == _name:
jpayne@69 555 self._headers[i] = self.policy.header_store_parse(k, _value)
jpayne@69 556 break
jpayne@69 557 else:
jpayne@69 558 raise KeyError(_name)
jpayne@69 559
jpayne@69 560 #
jpayne@69 561 # Use these three methods instead of the three above.
jpayne@69 562 #
jpayne@69 563
jpayne@69 564 def get_content_type(self):
jpayne@69 565 """Return the message's content type.
jpayne@69 566
jpayne@69 567 The returned string is coerced to lower case of the form
jpayne@69 568 `maintype/subtype'. If there was no Content-Type header in the
jpayne@69 569 message, the default type as given by get_default_type() will be
jpayne@69 570 returned. Since according to RFC 2045, messages always have a default
jpayne@69 571 type this will always return a value.
jpayne@69 572
jpayne@69 573 RFC 2045 defines a message's default type to be text/plain unless it
jpayne@69 574 appears inside a multipart/digest container, in which case it would be
jpayne@69 575 message/rfc822.
jpayne@69 576 """
jpayne@69 577 missing = object()
jpayne@69 578 value = self.get('content-type', missing)
jpayne@69 579 if value is missing:
jpayne@69 580 # This should have no parameters
jpayne@69 581 return self.get_default_type()
jpayne@69 582 ctype = _splitparam(value)[0].lower()
jpayne@69 583 # RFC 2045, section 5.2 says if its invalid, use text/plain
jpayne@69 584 if ctype.count('/') != 1:
jpayne@69 585 return 'text/plain'
jpayne@69 586 return ctype
jpayne@69 587
jpayne@69 588 def get_content_maintype(self):
jpayne@69 589 """Return the message's main content type.
jpayne@69 590
jpayne@69 591 This is the `maintype' part of the string returned by
jpayne@69 592 get_content_type().
jpayne@69 593 """
jpayne@69 594 ctype = self.get_content_type()
jpayne@69 595 return ctype.split('/')[0]
jpayne@69 596
jpayne@69 597 def get_content_subtype(self):
jpayne@69 598 """Returns the message's sub-content type.
jpayne@69 599
jpayne@69 600 This is the `subtype' part of the string returned by
jpayne@69 601 get_content_type().
jpayne@69 602 """
jpayne@69 603 ctype = self.get_content_type()
jpayne@69 604 return ctype.split('/')[1]
jpayne@69 605
jpayne@69 606 def get_default_type(self):
jpayne@69 607 """Return the `default' content type.
jpayne@69 608
jpayne@69 609 Most messages have a default content type of text/plain, except for
jpayne@69 610 messages that are subparts of multipart/digest containers. Such
jpayne@69 611 subparts have a default content type of message/rfc822.
jpayne@69 612 """
jpayne@69 613 return self._default_type
jpayne@69 614
jpayne@69 615 def set_default_type(self, ctype):
jpayne@69 616 """Set the `default' content type.
jpayne@69 617
jpayne@69 618 ctype should be either "text/plain" or "message/rfc822", although this
jpayne@69 619 is not enforced. The default content type is not stored in the
jpayne@69 620 Content-Type header.
jpayne@69 621 """
jpayne@69 622 self._default_type = ctype
jpayne@69 623
jpayne@69 624 def _get_params_preserve(self, failobj, header):
jpayne@69 625 # Like get_params() but preserves the quoting of values. BAW:
jpayne@69 626 # should this be part of the public interface?
jpayne@69 627 missing = object()
jpayne@69 628 value = self.get(header, missing)
jpayne@69 629 if value is missing:
jpayne@69 630 return failobj
jpayne@69 631 params = []
jpayne@69 632 for p in _parseparam(value):
jpayne@69 633 try:
jpayne@69 634 name, val = p.split('=', 1)
jpayne@69 635 name = name.strip()
jpayne@69 636 val = val.strip()
jpayne@69 637 except ValueError:
jpayne@69 638 # Must have been a bare attribute
jpayne@69 639 name = p.strip()
jpayne@69 640 val = ''
jpayne@69 641 params.append((name, val))
jpayne@69 642 params = utils.decode_params(params)
jpayne@69 643 return params
jpayne@69 644
jpayne@69 645 def get_params(self, failobj=None, header='content-type', unquote=True):
jpayne@69 646 """Return the message's Content-Type parameters, as a list.
jpayne@69 647
jpayne@69 648 The elements of the returned list are 2-tuples of key/value pairs, as
jpayne@69 649 split on the `=' sign. The left hand side of the `=' is the key,
jpayne@69 650 while the right hand side is the value. If there is no `=' sign in
jpayne@69 651 the parameter the value is the empty string. The value is as
jpayne@69 652 described in the get_param() method.
jpayne@69 653
jpayne@69 654 Optional failobj is the object to return if there is no Content-Type
jpayne@69 655 header. Optional header is the header to search instead of
jpayne@69 656 Content-Type. If unquote is True, the value is unquoted.
jpayne@69 657 """
jpayne@69 658 missing = object()
jpayne@69 659 params = self._get_params_preserve(missing, header)
jpayne@69 660 if params is missing:
jpayne@69 661 return failobj
jpayne@69 662 if unquote:
jpayne@69 663 return [(k, _unquotevalue(v)) for k, v in params]
jpayne@69 664 else:
jpayne@69 665 return params
jpayne@69 666
jpayne@69 667 def get_param(self, param, failobj=None, header='content-type',
jpayne@69 668 unquote=True):
jpayne@69 669 """Return the parameter value if found in the Content-Type header.
jpayne@69 670
jpayne@69 671 Optional failobj is the object to return if there is no Content-Type
jpayne@69 672 header, or the Content-Type header has no such parameter. Optional
jpayne@69 673 header is the header to search instead of Content-Type.
jpayne@69 674
jpayne@69 675 Parameter keys are always compared case insensitively. The return
jpayne@69 676 value can either be a string, or a 3-tuple if the parameter was RFC
jpayne@69 677 2231 encoded. When it's a 3-tuple, the elements of the value are of
jpayne@69 678 the form (CHARSET, LANGUAGE, VALUE). Note that both CHARSET and
jpayne@69 679 LANGUAGE can be None, in which case you should consider VALUE to be
jpayne@69 680 encoded in the us-ascii charset. You can usually ignore LANGUAGE.
jpayne@69 681 The parameter value (either the returned string, or the VALUE item in
jpayne@69 682 the 3-tuple) is always unquoted, unless unquote is set to False.
jpayne@69 683
jpayne@69 684 If your application doesn't care whether the parameter was RFC 2231
jpayne@69 685 encoded, it can turn the return value into a string as follows:
jpayne@69 686
jpayne@69 687 rawparam = msg.get_param('foo')
jpayne@69 688 param = email.utils.collapse_rfc2231_value(rawparam)
jpayne@69 689
jpayne@69 690 """
jpayne@69 691 if header not in self:
jpayne@69 692 return failobj
jpayne@69 693 for k, v in self._get_params_preserve(failobj, header):
jpayne@69 694 if k.lower() == param.lower():
jpayne@69 695 if unquote:
jpayne@69 696 return _unquotevalue(v)
jpayne@69 697 else:
jpayne@69 698 return v
jpayne@69 699 return failobj
jpayne@69 700
jpayne@69 701 def set_param(self, param, value, header='Content-Type', requote=True,
jpayne@69 702 charset=None, language='', replace=False):
jpayne@69 703 """Set a parameter in the Content-Type header.
jpayne@69 704
jpayne@69 705 If the parameter already exists in the header, its value will be
jpayne@69 706 replaced with the new value.
jpayne@69 707
jpayne@69 708 If header is Content-Type and has not yet been defined for this
jpayne@69 709 message, it will be set to "text/plain" and the new parameter and
jpayne@69 710 value will be appended as per RFC 2045.
jpayne@69 711
jpayne@69 712 An alternate header can be specified in the header argument, and all
jpayne@69 713 parameters will be quoted as necessary unless requote is False.
jpayne@69 714
jpayne@69 715 If charset is specified, the parameter will be encoded according to RFC
jpayne@69 716 2231. Optional language specifies the RFC 2231 language, defaulting
jpayne@69 717 to the empty string. Both charset and language should be strings.
jpayne@69 718 """
jpayne@69 719 if not isinstance(value, tuple) and charset:
jpayne@69 720 value = (charset, language, value)
jpayne@69 721
jpayne@69 722 if header not in self and header.lower() == 'content-type':
jpayne@69 723 ctype = 'text/plain'
jpayne@69 724 else:
jpayne@69 725 ctype = self.get(header)
jpayne@69 726 if not self.get_param(param, header=header):
jpayne@69 727 if not ctype:
jpayne@69 728 ctype = _formatparam(param, value, requote)
jpayne@69 729 else:
jpayne@69 730 ctype = SEMISPACE.join(
jpayne@69 731 [ctype, _formatparam(param, value, requote)])
jpayne@69 732 else:
jpayne@69 733 ctype = ''
jpayne@69 734 for old_param, old_value in self.get_params(header=header,
jpayne@69 735 unquote=requote):
jpayne@69 736 append_param = ''
jpayne@69 737 if old_param.lower() == param.lower():
jpayne@69 738 append_param = _formatparam(param, value, requote)
jpayne@69 739 else:
jpayne@69 740 append_param = _formatparam(old_param, old_value, requote)
jpayne@69 741 if not ctype:
jpayne@69 742 ctype = append_param
jpayne@69 743 else:
jpayne@69 744 ctype = SEMISPACE.join([ctype, append_param])
jpayne@69 745 if ctype != self.get(header):
jpayne@69 746 if replace:
jpayne@69 747 self.replace_header(header, ctype)
jpayne@69 748 else:
jpayne@69 749 del self[header]
jpayne@69 750 self[header] = ctype
jpayne@69 751
jpayne@69 752 def del_param(self, param, header='content-type', requote=True):
jpayne@69 753 """Remove the given parameter completely from the Content-Type header.
jpayne@69 754
jpayne@69 755 The header will be re-written in place without the parameter or its
jpayne@69 756 value. All values will be quoted as necessary unless requote is
jpayne@69 757 False. Optional header specifies an alternative to the Content-Type
jpayne@69 758 header.
jpayne@69 759 """
jpayne@69 760 if header not in self:
jpayne@69 761 return
jpayne@69 762 new_ctype = ''
jpayne@69 763 for p, v in self.get_params(header=header, unquote=requote):
jpayne@69 764 if p.lower() != param.lower():
jpayne@69 765 if not new_ctype:
jpayne@69 766 new_ctype = _formatparam(p, v, requote)
jpayne@69 767 else:
jpayne@69 768 new_ctype = SEMISPACE.join([new_ctype,
jpayne@69 769 _formatparam(p, v, requote)])
jpayne@69 770 if new_ctype != self.get(header):
jpayne@69 771 del self[header]
jpayne@69 772 self[header] = new_ctype
jpayne@69 773
jpayne@69 774 def set_type(self, type, header='Content-Type', requote=True):
jpayne@69 775 """Set the main type and subtype for the Content-Type header.
jpayne@69 776
jpayne@69 777 type must be a string in the form "maintype/subtype", otherwise a
jpayne@69 778 ValueError is raised.
jpayne@69 779
jpayne@69 780 This method replaces the Content-Type header, keeping all the
jpayne@69 781 parameters in place. If requote is False, this leaves the existing
jpayne@69 782 header's quoting as is. Otherwise, the parameters will be quoted (the
jpayne@69 783 default).
jpayne@69 784
jpayne@69 785 An alternative header can be specified in the header argument. When
jpayne@69 786 the Content-Type header is set, we'll always also add a MIME-Version
jpayne@69 787 header.
jpayne@69 788 """
jpayne@69 789 # BAW: should we be strict?
jpayne@69 790 if not type.count('/') == 1:
jpayne@69 791 raise ValueError
jpayne@69 792 # Set the Content-Type, you get a MIME-Version
jpayne@69 793 if header.lower() == 'content-type':
jpayne@69 794 del self['mime-version']
jpayne@69 795 self['MIME-Version'] = '1.0'
jpayne@69 796 if header not in self:
jpayne@69 797 self[header] = type
jpayne@69 798 return
jpayne@69 799 params = self.get_params(header=header, unquote=requote)
jpayne@69 800 del self[header]
jpayne@69 801 self[header] = type
jpayne@69 802 # Skip the first param; it's the old type.
jpayne@69 803 for p, v in params[1:]:
jpayne@69 804 self.set_param(p, v, header, requote)
jpayne@69 805
jpayne@69 806 def get_filename(self, failobj=None):
jpayne@69 807 """Return the filename associated with the payload if present.
jpayne@69 808
jpayne@69 809 The filename is extracted from the Content-Disposition header's
jpayne@69 810 `filename' parameter, and it is unquoted. If that header is missing
jpayne@69 811 the `filename' parameter, this method falls back to looking for the
jpayne@69 812 `name' parameter.
jpayne@69 813 """
jpayne@69 814 missing = object()
jpayne@69 815 filename = self.get_param('filename', missing, 'content-disposition')
jpayne@69 816 if filename is missing:
jpayne@69 817 filename = self.get_param('name', missing, 'content-type')
jpayne@69 818 if filename is missing:
jpayne@69 819 return failobj
jpayne@69 820 return utils.collapse_rfc2231_value(filename).strip()
jpayne@69 821
jpayne@69 822 def get_boundary(self, failobj=None):
jpayne@69 823 """Return the boundary associated with the payload if present.
jpayne@69 824
jpayne@69 825 The boundary is extracted from the Content-Type header's `boundary'
jpayne@69 826 parameter, and it is unquoted.
jpayne@69 827 """
jpayne@69 828 missing = object()
jpayne@69 829 boundary = self.get_param('boundary', missing)
jpayne@69 830 if boundary is missing:
jpayne@69 831 return failobj
jpayne@69 832 # RFC 2046 says that boundaries may begin but not end in w/s
jpayne@69 833 return utils.collapse_rfc2231_value(boundary).rstrip()
jpayne@69 834
jpayne@69 835 def set_boundary(self, boundary):
jpayne@69 836 """Set the boundary parameter in Content-Type to 'boundary'.
jpayne@69 837
jpayne@69 838 This is subtly different than deleting the Content-Type header and
jpayne@69 839 adding a new one with a new boundary parameter via add_header(). The
jpayne@69 840 main difference is that using the set_boundary() method preserves the
jpayne@69 841 order of the Content-Type header in the original message.
jpayne@69 842
jpayne@69 843 HeaderParseError is raised if the message has no Content-Type header.
jpayne@69 844 """
jpayne@69 845 missing = object()
jpayne@69 846 params = self._get_params_preserve(missing, 'content-type')
jpayne@69 847 if params is missing:
jpayne@69 848 # There was no Content-Type header, and we don't know what type
jpayne@69 849 # to set it to, so raise an exception.
jpayne@69 850 raise errors.HeaderParseError('No Content-Type header found')
jpayne@69 851 newparams = []
jpayne@69 852 foundp = False
jpayne@69 853 for pk, pv in params:
jpayne@69 854 if pk.lower() == 'boundary':
jpayne@69 855 newparams.append(('boundary', '"%s"' % boundary))
jpayne@69 856 foundp = True
jpayne@69 857 else:
jpayne@69 858 newparams.append((pk, pv))
jpayne@69 859 if not foundp:
jpayne@69 860 # The original Content-Type header had no boundary attribute.
jpayne@69 861 # Tack one on the end. BAW: should we raise an exception
jpayne@69 862 # instead???
jpayne@69 863 newparams.append(('boundary', '"%s"' % boundary))
jpayne@69 864 # Replace the existing Content-Type header with the new value
jpayne@69 865 newheaders = []
jpayne@69 866 for h, v in self._headers:
jpayne@69 867 if h.lower() == 'content-type':
jpayne@69 868 parts = []
jpayne@69 869 for k, v in newparams:
jpayne@69 870 if v == '':
jpayne@69 871 parts.append(k)
jpayne@69 872 else:
jpayne@69 873 parts.append('%s=%s' % (k, v))
jpayne@69 874 val = SEMISPACE.join(parts)
jpayne@69 875 newheaders.append(self.policy.header_store_parse(h, val))
jpayne@69 876
jpayne@69 877 else:
jpayne@69 878 newheaders.append((h, v))
jpayne@69 879 self._headers = newheaders
jpayne@69 880
jpayne@69 881 def get_content_charset(self, failobj=None):
jpayne@69 882 """Return the charset parameter of the Content-Type header.
jpayne@69 883
jpayne@69 884 The returned string is always coerced to lower case. If there is no
jpayne@69 885 Content-Type header, or if that header has no charset parameter,
jpayne@69 886 failobj is returned.
jpayne@69 887 """
jpayne@69 888 missing = object()
jpayne@69 889 charset = self.get_param('charset', missing)
jpayne@69 890 if charset is missing:
jpayne@69 891 return failobj
jpayne@69 892 if isinstance(charset, tuple):
jpayne@69 893 # RFC 2231 encoded, so decode it, and it better end up as ascii.
jpayne@69 894 pcharset = charset[0] or 'us-ascii'
jpayne@69 895 try:
jpayne@69 896 # LookupError will be raised if the charset isn't known to
jpayne@69 897 # Python. UnicodeError will be raised if the encoded text
jpayne@69 898 # contains a character not in the charset.
jpayne@69 899 as_bytes = charset[2].encode('raw-unicode-escape')
jpayne@69 900 charset = str(as_bytes, pcharset)
jpayne@69 901 except (LookupError, UnicodeError):
jpayne@69 902 charset = charset[2]
jpayne@69 903 # charset characters must be in us-ascii range
jpayne@69 904 try:
jpayne@69 905 charset.encode('us-ascii')
jpayne@69 906 except UnicodeError:
jpayne@69 907 return failobj
jpayne@69 908 # RFC 2046, $4.1.2 says charsets are not case sensitive
jpayne@69 909 return charset.lower()
jpayne@69 910
jpayne@69 911 def get_charsets(self, failobj=None):
jpayne@69 912 """Return a list containing the charset(s) used in this message.
jpayne@69 913
jpayne@69 914 The returned list of items describes the Content-Type headers'
jpayne@69 915 charset parameter for this message and all the subparts in its
jpayne@69 916 payload.
jpayne@69 917
jpayne@69 918 Each item will either be a string (the value of the charset parameter
jpayne@69 919 in the Content-Type header of that part) or the value of the
jpayne@69 920 'failobj' parameter (defaults to None), if the part does not have a
jpayne@69 921 main MIME type of "text", or the charset is not defined.
jpayne@69 922
jpayne@69 923 The list will contain one string for each part of the message, plus
jpayne@69 924 one for the container message (i.e. self), so that a non-multipart
jpayne@69 925 message will still return a list of length 1.
jpayne@69 926 """
jpayne@69 927 return [part.get_content_charset(failobj) for part in self.walk()]
jpayne@69 928
jpayne@69 929 def get_content_disposition(self):
jpayne@69 930 """Return the message's content-disposition if it exists, or None.
jpayne@69 931
jpayne@69 932 The return values can be either 'inline', 'attachment' or None
jpayne@69 933 according to the rfc2183.
jpayne@69 934 """
jpayne@69 935 value = self.get('content-disposition')
jpayne@69 936 if value is None:
jpayne@69 937 return None
jpayne@69 938 c_d = _splitparam(value)[0].lower()
jpayne@69 939 return c_d
jpayne@69 940
jpayne@69 941 # I.e. def walk(self): ...
jpayne@69 942 from email.iterators import walk
jpayne@69 943
jpayne@69 944
jpayne@69 945 class MIMEPart(Message):
jpayne@69 946
jpayne@69 947 def __init__(self, policy=None):
jpayne@69 948 if policy is None:
jpayne@69 949 from email.policy import default
jpayne@69 950 policy = default
jpayne@69 951 Message.__init__(self, policy)
jpayne@69 952
jpayne@69 953
jpayne@69 954 def as_string(self, unixfrom=False, maxheaderlen=None, policy=None):
jpayne@69 955 """Return the entire formatted message as a string.
jpayne@69 956
jpayne@69 957 Optional 'unixfrom', when true, means include the Unix From_ envelope
jpayne@69 958 header. maxheaderlen is retained for backward compatibility with the
jpayne@69 959 base Message class, but defaults to None, meaning that the policy value
jpayne@69 960 for max_line_length controls the header maximum length. 'policy' is
jpayne@69 961 passed to the Generator instance used to serialize the mesasge; if it
jpayne@69 962 is not specified the policy associated with the message instance is
jpayne@69 963 used.
jpayne@69 964 """
jpayne@69 965 policy = self.policy if policy is None else policy
jpayne@69 966 if maxheaderlen is None:
jpayne@69 967 maxheaderlen = policy.max_line_length
jpayne@69 968 return super().as_string(maxheaderlen=maxheaderlen, policy=policy)
jpayne@69 969
jpayne@69 970 def __str__(self):
jpayne@69 971 return self.as_string(policy=self.policy.clone(utf8=True))
jpayne@69 972
jpayne@69 973 def is_attachment(self):
jpayne@69 974 c_d = self.get('content-disposition')
jpayne@69 975 return False if c_d is None else c_d.content_disposition == 'attachment'
jpayne@69 976
jpayne@69 977 def _find_body(self, part, preferencelist):
jpayne@69 978 if part.is_attachment():
jpayne@69 979 return
jpayne@69 980 maintype, subtype = part.get_content_type().split('/')
jpayne@69 981 if maintype == 'text':
jpayne@69 982 if subtype in preferencelist:
jpayne@69 983 yield (preferencelist.index(subtype), part)
jpayne@69 984 return
jpayne@69 985 if maintype != 'multipart':
jpayne@69 986 return
jpayne@69 987 if subtype != 'related':
jpayne@69 988 for subpart in part.iter_parts():
jpayne@69 989 yield from self._find_body(subpart, preferencelist)
jpayne@69 990 return
jpayne@69 991 if 'related' in preferencelist:
jpayne@69 992 yield (preferencelist.index('related'), part)
jpayne@69 993 candidate = None
jpayne@69 994 start = part.get_param('start')
jpayne@69 995 if start:
jpayne@69 996 for subpart in part.iter_parts():
jpayne@69 997 if subpart['content-id'] == start:
jpayne@69 998 candidate = subpart
jpayne@69 999 break
jpayne@69 1000 if candidate is None:
jpayne@69 1001 subparts = part.get_payload()
jpayne@69 1002 candidate = subparts[0] if subparts else None
jpayne@69 1003 if candidate is not None:
jpayne@69 1004 yield from self._find_body(candidate, preferencelist)
jpayne@69 1005
jpayne@69 1006 def get_body(self, preferencelist=('related', 'html', 'plain')):
jpayne@69 1007 """Return best candidate mime part for display as 'body' of message.
jpayne@69 1008
jpayne@69 1009 Do a depth first search, starting with self, looking for the first part
jpayne@69 1010 matching each of the items in preferencelist, and return the part
jpayne@69 1011 corresponding to the first item that has a match, or None if no items
jpayne@69 1012 have a match. If 'related' is not included in preferencelist, consider
jpayne@69 1013 the root part of any multipart/related encountered as a candidate
jpayne@69 1014 match. Ignore parts with 'Content-Disposition: attachment'.
jpayne@69 1015 """
jpayne@69 1016 best_prio = len(preferencelist)
jpayne@69 1017 body = None
jpayne@69 1018 for prio, part in self._find_body(self, preferencelist):
jpayne@69 1019 if prio < best_prio:
jpayne@69 1020 best_prio = prio
jpayne@69 1021 body = part
jpayne@69 1022 if prio == 0:
jpayne@69 1023 break
jpayne@69 1024 return body
jpayne@69 1025
jpayne@69 1026 _body_types = {('text', 'plain'),
jpayne@69 1027 ('text', 'html'),
jpayne@69 1028 ('multipart', 'related'),
jpayne@69 1029 ('multipart', 'alternative')}
jpayne@69 1030 def iter_attachments(self):
jpayne@69 1031 """Return an iterator over the non-main parts of a multipart.
jpayne@69 1032
jpayne@69 1033 Skip the first of each occurrence of text/plain, text/html,
jpayne@69 1034 multipart/related, or multipart/alternative in the multipart (unless
jpayne@69 1035 they have a 'Content-Disposition: attachment' header) and include all
jpayne@69 1036 remaining subparts in the returned iterator. When applied to a
jpayne@69 1037 multipart/related, return all parts except the root part. Return an
jpayne@69 1038 empty iterator when applied to a multipart/alternative or a
jpayne@69 1039 non-multipart.
jpayne@69 1040 """
jpayne@69 1041 maintype, subtype = self.get_content_type().split('/')
jpayne@69 1042 if maintype != 'multipart' or subtype == 'alternative':
jpayne@69 1043 return
jpayne@69 1044 payload = self.get_payload()
jpayne@69 1045 # Certain malformed messages can have content type set to `multipart/*`
jpayne@69 1046 # but still have single part body, in which case payload.copy() can
jpayne@69 1047 # fail with AttributeError.
jpayne@69 1048 try:
jpayne@69 1049 parts = payload.copy()
jpayne@69 1050 except AttributeError:
jpayne@69 1051 # payload is not a list, it is most probably a string.
jpayne@69 1052 return
jpayne@69 1053
jpayne@69 1054 if maintype == 'multipart' and subtype == 'related':
jpayne@69 1055 # For related, we treat everything but the root as an attachment.
jpayne@69 1056 # The root may be indicated by 'start'; if there's no start or we
jpayne@69 1057 # can't find the named start, treat the first subpart as the root.
jpayne@69 1058 start = self.get_param('start')
jpayne@69 1059 if start:
jpayne@69 1060 found = False
jpayne@69 1061 attachments = []
jpayne@69 1062 for part in parts:
jpayne@69 1063 if part.get('content-id') == start:
jpayne@69 1064 found = True
jpayne@69 1065 else:
jpayne@69 1066 attachments.append(part)
jpayne@69 1067 if found:
jpayne@69 1068 yield from attachments
jpayne@69 1069 return
jpayne@69 1070 parts.pop(0)
jpayne@69 1071 yield from parts
jpayne@69 1072 return
jpayne@69 1073 # Otherwise we more or less invert the remaining logic in get_body.
jpayne@69 1074 # This only really works in edge cases (ex: non-text related or
jpayne@69 1075 # alternatives) if the sending agent sets content-disposition.
jpayne@69 1076 seen = [] # Only skip the first example of each candidate type.
jpayne@69 1077 for part in parts:
jpayne@69 1078 maintype, subtype = part.get_content_type().split('/')
jpayne@69 1079 if ((maintype, subtype) in self._body_types and
jpayne@69 1080 not part.is_attachment() and subtype not in seen):
jpayne@69 1081 seen.append(subtype)
jpayne@69 1082 continue
jpayne@69 1083 yield part
jpayne@69 1084
jpayne@69 1085 def iter_parts(self):
jpayne@69 1086 """Return an iterator over all immediate subparts of a multipart.
jpayne@69 1087
jpayne@69 1088 Return an empty iterator for a non-multipart.
jpayne@69 1089 """
jpayne@69 1090 if self.get_content_maintype() == 'multipart':
jpayne@69 1091 yield from self.get_payload()
jpayne@69 1092
jpayne@69 1093 def get_content(self, *args, content_manager=None, **kw):
jpayne@69 1094 if content_manager is None:
jpayne@69 1095 content_manager = self.policy.content_manager
jpayne@69 1096 return content_manager.get_content(self, *args, **kw)
jpayne@69 1097
jpayne@69 1098 def set_content(self, *args, content_manager=None, **kw):
jpayne@69 1099 if content_manager is None:
jpayne@69 1100 content_manager = self.policy.content_manager
jpayne@69 1101 content_manager.set_content(self, *args, **kw)
jpayne@69 1102
jpayne@69 1103 def _make_multipart(self, subtype, disallowed_subtypes, boundary):
jpayne@69 1104 if self.get_content_maintype() == 'multipart':
jpayne@69 1105 existing_subtype = self.get_content_subtype()
jpayne@69 1106 disallowed_subtypes = disallowed_subtypes + (subtype,)
jpayne@69 1107 if existing_subtype in disallowed_subtypes:
jpayne@69 1108 raise ValueError("Cannot convert {} to {}".format(
jpayne@69 1109 existing_subtype, subtype))
jpayne@69 1110 keep_headers = []
jpayne@69 1111 part_headers = []
jpayne@69 1112 for name, value in self._headers:
jpayne@69 1113 if name.lower().startswith('content-'):
jpayne@69 1114 part_headers.append((name, value))
jpayne@69 1115 else:
jpayne@69 1116 keep_headers.append((name, value))
jpayne@69 1117 if part_headers:
jpayne@69 1118 # There is existing content, move it to the first subpart.
jpayne@69 1119 part = type(self)(policy=self.policy)
jpayne@69 1120 part._headers = part_headers
jpayne@69 1121 part._payload = self._payload
jpayne@69 1122 self._payload = [part]
jpayne@69 1123 else:
jpayne@69 1124 self._payload = []
jpayne@69 1125 self._headers = keep_headers
jpayne@69 1126 self['Content-Type'] = 'multipart/' + subtype
jpayne@69 1127 if boundary is not None:
jpayne@69 1128 self.set_param('boundary', boundary)
jpayne@69 1129
jpayne@69 1130 def make_related(self, boundary=None):
jpayne@69 1131 self._make_multipart('related', ('alternative', 'mixed'), boundary)
jpayne@69 1132
jpayne@69 1133 def make_alternative(self, boundary=None):
jpayne@69 1134 self._make_multipart('alternative', ('mixed',), boundary)
jpayne@69 1135
jpayne@69 1136 def make_mixed(self, boundary=None):
jpayne@69 1137 self._make_multipart('mixed', (), boundary)
jpayne@69 1138
jpayne@69 1139 def _add_multipart(self, _subtype, *args, _disp=None, **kw):
jpayne@69 1140 if (self.get_content_maintype() != 'multipart' or
jpayne@69 1141 self.get_content_subtype() != _subtype):
jpayne@69 1142 getattr(self, 'make_' + _subtype)()
jpayne@69 1143 part = type(self)(policy=self.policy)
jpayne@69 1144 part.set_content(*args, **kw)
jpayne@69 1145 if _disp and 'content-disposition' not in part:
jpayne@69 1146 part['Content-Disposition'] = _disp
jpayne@69 1147 self.attach(part)
jpayne@69 1148
jpayne@69 1149 def add_related(self, *args, **kw):
jpayne@69 1150 self._add_multipart('related', *args, _disp='inline', **kw)
jpayne@69 1151
jpayne@69 1152 def add_alternative(self, *args, **kw):
jpayne@69 1153 self._add_multipart('alternative', *args, **kw)
jpayne@69 1154
jpayne@69 1155 def add_attachment(self, *args, **kw):
jpayne@69 1156 self._add_multipart('mixed', *args, _disp='attachment', **kw)
jpayne@69 1157
jpayne@69 1158 def clear(self):
jpayne@69 1159 self._headers = []
jpayne@69 1160 self._payload = None
jpayne@69 1161
jpayne@69 1162 def clear_content(self):
jpayne@69 1163 self._headers = [(n, v) for n, v in self._headers
jpayne@69 1164 if not n.lower().startswith('content-')]
jpayne@69 1165 self._payload = None
jpayne@69 1166
jpayne@69 1167
jpayne@69 1168 class EmailMessage(MIMEPart):
jpayne@69 1169
jpayne@69 1170 def set_content(self, *args, **kw):
jpayne@69 1171 super().set_content(*args, **kw)
jpayne@69 1172 if 'MIME-Version' not in self:
jpayne@69 1173 self['MIME-Version'] = '1.0'