mirror of
https://github.com/python/cpython.git
synced 2025-08-04 00:48:58 +00:00
#18891: Complete new provisional email API.
This adds EmailMessage and, MIMEPart subclasses of Message with new API methods, and a ContentManager class used by the new methods. Also a new policy setting, content_manager. Patch was reviewed by Stephen J. Turnbull and Serhiy Storchaka, and reflects their feedback. I will ideally add some examples of using the new API to the documentation before the final release.
This commit is contained in:
parent
1a16288197
commit
3da240fd01
15 changed files with 2539 additions and 26 deletions
|
@ -8,8 +8,6 @@ __all__ = ['Message']
|
|||
|
||||
import re
|
||||
import uu
|
||||
import base64
|
||||
import binascii
|
||||
from io import BytesIO, StringIO
|
||||
|
||||
# Intrapackage imports
|
||||
|
@ -679,7 +677,7 @@ class Message:
|
|||
return failobj
|
||||
|
||||
def set_param(self, param, value, header='Content-Type', requote=True,
|
||||
charset=None, language=''):
|
||||
charset=None, language='', replace=False):
|
||||
"""Set a parameter in the Content-Type header.
|
||||
|
||||
If the parameter already exists in the header, its value will be
|
||||
|
@ -723,8 +721,11 @@ class Message:
|
|||
else:
|
||||
ctype = SEMISPACE.join([ctype, append_param])
|
||||
if ctype != self.get(header):
|
||||
del self[header]
|
||||
self[header] = ctype
|
||||
if replace:
|
||||
self.replace_header(header, ctype)
|
||||
else:
|
||||
del self[header]
|
||||
self[header] = ctype
|
||||
|
||||
def del_param(self, param, header='content-type', requote=True):
|
||||
"""Remove the given parameter completely from the Content-Type header.
|
||||
|
@ -905,3 +906,208 @@ class Message:
|
|||
|
||||
# I.e. def walk(self): ...
|
||||
from email.iterators import walk
|
||||
|
||||
|
||||
class MIMEPart(Message):
|
||||
|
||||
def __init__(self, policy=None):
|
||||
if policy is None:
|
||||
from email.policy import default
|
||||
policy = default
|
||||
Message.__init__(self, policy)
|
||||
|
||||
@property
|
||||
def is_attachment(self):
|
||||
c_d = self.get('content-disposition')
|
||||
if c_d is None:
|
||||
return False
|
||||
return c_d.lower() == 'attachment'
|
||||
|
||||
def _find_body(self, part, preferencelist):
|
||||
if part.is_attachment:
|
||||
return
|
||||
maintype, subtype = part.get_content_type().split('/')
|
||||
if maintype == 'text':
|
||||
if subtype in preferencelist:
|
||||
yield (preferencelist.index(subtype), part)
|
||||
return
|
||||
if maintype != 'multipart':
|
||||
return
|
||||
if subtype != 'related':
|
||||
for subpart in part.iter_parts():
|
||||
yield from self._find_body(subpart, preferencelist)
|
||||
return
|
||||
if 'related' in preferencelist:
|
||||
yield (preferencelist.index('related'), part)
|
||||
candidate = None
|
||||
start = part.get_param('start')
|
||||
if start:
|
||||
for subpart in part.iter_parts():
|
||||
if subpart['content-id'] == start:
|
||||
candidate = subpart
|
||||
break
|
||||
if candidate is None:
|
||||
subparts = part.get_payload()
|
||||
candidate = subparts[0] if subparts else None
|
||||
if 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 part
|
||||
matching each of the items in preferencelist, and return the part
|
||||
corresponding to the first item that has a match, or None if no items
|
||||
have a match. If 'related' is not included in preferencelist, consider
|
||||
the root part of any multipart/related encountered as a candidate
|
||||
match. Ignore parts with 'Content-Disposition: attachment'.
|
||||
"""
|
||||
best_prio = len(preferencelist)
|
||||
body = None
|
||||
for prio, part in self._find_body(self, preferencelist):
|
||||
if prio < best_prio:
|
||||
best_prio = prio
|
||||
body = part
|
||||
if prio == 0:
|
||||
break
|
||||
return 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 (unless
|
||||
they have a 'Content-Disposition: attachment' header) and include all
|
||||
remaining subparts in the returned iterator. When applied to a
|
||||
multipart/related, return all parts except the root part. Return an
|
||||
empty iterator when applied to a multipart/alternative or a
|
||||
non-multipart.
|
||||
"""
|
||||
maintype, subtype = self.get_content_type().split('/')
|
||||
if maintype != 'multipart' or subtype == 'alternative':
|
||||
return
|
||||
parts = self.get_payload()
|
||||
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 = False
|
||||
attachments = []
|
||||
for part in parts:
|
||||
if part.get('content-id') == start:
|
||||
found = True
|
||||
else:
|
||||
attachments.append(part)
|
||||
if found:
|
||||
yield from attachments
|
||||
return
|
||||
parts.pop(0)
|
||||
yield from parts
|
||||
return
|
||||
# Otherwise we more or less invert the remaining logic in get_body.
|
||||
# This only really works in edge cases (ex: non-text relateds 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 and
|
||||
not part.is_attachment and subtype not in seen):
|
||||
seen.append(subtype)
|
||||
continue
|
||||
yield part
|
||||
|
||||
def 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_manager
|
||||
return 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_manager
|
||||
content_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_headers
|
||||
part._payload = self._payload
|
||||
self._payload = [part]
|
||||
else:
|
||||
self._payload = []
|
||||
self._headers = keep_headers
|
||||
self['Content-Type'] = 'multipart/' + subtype
|
||||
if 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' or
|
||||
self.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'] = _disp
|
||||
self.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 = None
|
||||
|
||||
def clear_content(self):
|
||||
self._headers = [(n, v) for n, v in self._headers
|
||||
if not n.lower().startswith('content-')]
|
||||
self._payload = None
|
||||
|
||||
|
||||
class EmailMessage(MIMEPart):
|
||||
|
||||
def set_content(self, *args, **kw):
|
||||
super().set_content(*args, **kw)
|
||||
if 'MIME-Version' not in self:
|
||||
self['MIME-Version'] = '1.0'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue