diff --git a/Doc/library/email.mime.rst b/Doc/library/email.mime.rst --- a/Doc/library/email.mime.rst +++ b/Doc/library/email.mime.rst @@ -20,36 +20,43 @@ You can create a new object structure by instances, adding attachments and all the appropriate headers manually. For MIME messages though, the :mod:`email` package provides some convenient subclasses to make things easier. Here are the classes: .. currentmodule:: email.mime.base -.. class:: MIMEBase(_maintype, _subtype, **_params) +.. class:: MIMEBase(_maintype, _subtype, *, policy=compat32, **_params) Module: :mod:`email.mime.base` This is the base class for all the MIME-specific subclasses of :class:`~email.message.Message`. Ordinarily you won't create instances specifically of :class:`MIMEBase`, although you could. :class:`MIMEBase` is provided primarily as a convenient base class for more specific MIME-aware subclasses. *_maintype* is the :mailheader:`Content-Type` major type (e.g. :mimetype:`text` or :mimetype:`image`), and *_subtype* is the :mailheader:`Content-Type` minor type (e.g. :mimetype:`plain` or :mimetype:`gif`). *_params* is a parameter key/value dictionary and is passed directly to :meth:`Message.add_header `. + If *policy* is specified, (defaults to the + :class:`compat32 ` policy) it will be passed to + :class:`~email.message.Message`. + The :class:`MIMEBase` class always adds a :mailheader:`Content-Type` header (based on *_maintype*, *_subtype*, and *_params*), and a :mailheader:`MIME-Version` header (always set to ``1.0``). + .. versionchanged:: 3.6 + Added *policy* keyword-only parameter. + .. currentmodule:: email.mime.nonmultipart .. class:: MIMENonMultipart() Module: :mod:`email.mime.nonmultipart` A subclass of :class:`~email.mime.base.MIMEBase`, this is an intermediate base @@ -57,17 +64,17 @@ Here are the classes: purpose of this class is to prevent the use of the :meth:`~email.message.Message.attach` method, which only makes sense for :mimetype:`multipart` messages. If :meth:`~email.message.Message.attach` is called, a :exc:`~email.errors.MultipartConversionError` exception is raised. .. currentmodule:: email.mime.multipart -.. class:: MIMEMultipart(_subtype='mixed', boundary=None, _subparts=None, **_params) +.. class:: MIMEMultipart(_subtype='mixed', boundary=None, _subparts=None, *, policy=compat32, **_params) Module: :mod:`email.mime.multipart` A subclass of :class:`~email.mime.base.MIMEBase`, this is an intermediate base class for MIME messages that are :mimetype:`multipart`. Optional *_subtype* defaults to :mimetype:`mixed`, but can be used to specify the subtype of the message. A :mailheader:`Content-Type` header of :mimetype:`multipart/_subtype` will be added to the message object. A :mailheader:`MIME-Version` header will @@ -77,24 +84,28 @@ Here are the classes: default), the boundary is calculated when needed (for example, when the message is serialized). *_subparts* is a sequence of initial subparts for the payload. It must be possible to convert this sequence to a list. You can always attach new subparts to the message by using the :meth:`Message.attach ` method. + Optional *policy* argument defaults to :class:`compat32 `. + Additional parameters for the :mailheader:`Content-Type` header are taken from the keyword arguments, or passed into the *_params* argument, which is a keyword dictionary. + .. versionchanged:: 3.6 + Added *policy* keyword-only parameter. .. currentmodule:: email.mime.application -.. class:: MIMEApplication(_data, _subtype='octet-stream', _encoder=email.encoders.encode_base64, **_params) +.. class:: MIMEApplication(_data, _subtype='octet-stream', _encoder=email.encoders.encode_base64, *, policy=compat32, **_params) Module: :mod:`email.mime.application` A subclass of :class:`~email.mime.nonmultipart.MIMENonMultipart`, the :class:`MIMEApplication` class is used to represent MIME message objects of major type :mimetype:`application`. *_data* is a string containing the raw byte data. Optional *_subtype* specifies the MIME subtype and defaults to :mimetype:`octet-stream`. @@ -104,22 +115,26 @@ Here are the classes: the :class:`MIMEApplication` instance. It should use :meth:`~email.message.Message.get_payload` and :meth:`~email.message.Message.set_payload` to change the payload to encoded form. It should also add any :mailheader:`Content-Transfer-Encoding` or other headers to the message object as necessary. The default encoding is base64. See the :mod:`email.encoders` module for a list of the built-in encoders. + Optional *policy* argument defaults to :class:`compat32 `. + *_params* are passed straight through to the base class constructor. + .. versionchanged:: 3.6 + Added *policy* keyword-only parameter. .. currentmodule:: email.mime.audio -.. class:: MIMEAudio(_audiodata, _subtype=None, _encoder=email.encoders.encode_base64, **_params) +.. class:: MIMEAudio(_audiodata, _subtype=None, _encoder=email.encoders.encode_base64, *, policy=compat32, **_params) Module: :mod:`email.mime.audio` A subclass of :class:`~email.mime.nonmultipart.MIMENonMultipart`, the :class:`MIMEAudio` class is used to create MIME message objects of major type :mimetype:`audio`. *_audiodata* is a string containing the raw audio data. If this data can be decoded by the standard Python module :mod:`sndhdr`, then the subtype will be automatically included in the :mailheader:`Content-Type` header. @@ -132,22 +147,26 @@ Here are the classes: which is the :class:`MIMEAudio` instance. It should use :meth:`~email.message.Message.get_payload` and :meth:`~email.message.Message.set_payload` to change the payload to encoded form. It should also add any :mailheader:`Content-Transfer-Encoding` or other headers to the message object as necessary. The default encoding is base64. See the :mod:`email.encoders` module for a list of the built-in encoders. + Optional *policy* argument defaults to :class:`compat32 `. + *_params* are passed straight through to the base class constructor. + .. versionchanged:: 3.6 + Added *policy* keyword-only parameter. .. currentmodule:: email.mime.image -.. class:: MIMEImage(_imagedata, _subtype=None, _encoder=email.encoders.encode_base64, **_params) +.. class:: MIMEImage(_imagedata, _subtype=None, _encoder=email.encoders.encode_base64, *, policy=compat32, **_params) Module: :mod:`email.mime.image` A subclass of :class:`~email.mime.nonmultipart.MIMENonMultipart`, the :class:`MIMEImage` class is used to create MIME message objects of major type :mimetype:`image`. *_imagedata* is a string containing the raw image data. If this data can be decoded by the standard Python module :mod:`imghdr`, then the subtype will be automatically included in the :mailheader:`Content-Type` header. @@ -160,39 +179,47 @@ Here are the classes: which is the :class:`MIMEImage` instance. It should use :meth:`~email.message.Message.get_payload` and :meth:`~email.message.Message.set_payload` to change the payload to encoded form. It should also add any :mailheader:`Content-Transfer-Encoding` or other headers to the message object as necessary. The default encoding is base64. See the :mod:`email.encoders` module for a list of the built-in encoders. + Optional *policy* argument defaults to :class:`compat32 `. + *_params* are passed straight through to the :class:`~email.mime.base.MIMEBase` constructor. + .. versionchanged:: 3.6 + Added *policy* keyword-only parameter. .. currentmodule:: email.mime.message -.. class:: MIMEMessage(_msg, _subtype='rfc822') +.. class:: MIMEMessage(_msg, _subtype='rfc822', *, policy=compat32) Module: :mod:`email.mime.message` A subclass of :class:`~email.mime.nonmultipart.MIMENonMultipart`, the :class:`MIMEMessage` class is used to create MIME objects of main type :mimetype:`message`. *_msg* is used as the payload, and must be an instance of class :class:`~email.message.Message` (or a subclass thereof), otherwise a :exc:`TypeError` is raised. Optional *_subtype* sets the subtype of the message; it defaults to :mimetype:`rfc822`. + Optional *policy* argument defaults to :class:`compat32 `. + + .. versionchanged:: 3.6 + Added *policy* keyword-only parameter. .. currentmodule:: email.mime.text -.. class:: MIMEText(_text, _subtype='plain', _charset=None) +.. class:: MIMEText(_text, _subtype='plain', _charset=None, *, policy=compat32) Module: :mod:`email.mime.text` A subclass of :class:`~email.mime.nonmultipart.MIMENonMultipart`, the :class:`MIMEText` class is used to create MIME objects of major type :mimetype:`text`. *_text* is the string for the payload. *_subtype* is the minor type and defaults to :mimetype:`plain`. *_charset* is the character set of the text and is passed as an argument to the @@ -206,10 +233,15 @@ Here are the classes: with a ``charset`` parameter, and a :mailheader:`Content-Transfer-Endcoding` header. This means that a subsequent ``set_payload`` call will not result in an encoded payload, even if a charset is passed in the ``set_payload`` command. You can "reset" this behavior by deleting the ``Content-Transfer-Encoding`` header, after which a ``set_payload`` call will automatically encode the new payload (and add a new :mailheader:`Content-Transfer-Encoding` header). + Optional *policy* argument defaults to :class:`compat32 `. + .. versionchanged:: 3.5 *_charset* also accepts :class:`~email.charset.Charset` instances. + + .. versionchanged:: 3.6 + Added *policy* keyword-only parameter. diff --git a/Doc/whatsnew/3.6.rst b/Doc/whatsnew/3.6.rst --- a/Doc/whatsnew/3.6.rst +++ b/Doc/whatsnew/3.6.rst @@ -268,16 +268,24 @@ datetime -------- The :meth:`datetime.strftime() ` and :meth:`date.strftime() ` methods now support ISO 8601 date directives ``%G``, ``%u`` and ``%V``. (Contributed by Ashley Anderson in :issue:`12006`.) +email +----- + +:class:`email.mime.MIMEBase` and its subclasses now have a *policy* +keyword-only parameter. +(Contributed by Berker Peksag in :issue:`42`.) + + faulthandler ------------ On Windows, the :mod:`faulthandler` module now installs a handler for Windows exceptions: see :func:`faulthandler.enable`. (Contributed by Victor Stinner in :issue:`23848`.) diff --git a/Lib/email/mime/application.py b/Lib/email/mime/application.py --- a/Lib/email/mime/application.py +++ b/Lib/email/mime/application.py @@ -9,28 +9,29 @@ from email import encoders from email.mime.nonmultipart import MIMENonMultipart class MIMEApplication(MIMENonMultipart): """Class for generating application/* MIME documents.""" def __init__(self, _data, _subtype='octet-stream', - _encoder=encoders.encode_base64, **_params): + _encoder=encoders.encode_base64, *, policy=None, **_params): """Create an application/* type MIME document. _data is a string containing the raw application data. _subtype is the MIME content type subtype, defaulting to 'octet-stream'. _encoder is a function which will perform the actual encoding for transport of the application data, defaulting to base64 encoding. Any additional keyword arguments are passed to the base class constructor, which turns them into parameters on the Content-Type header. """ if _subtype is None: raise TypeError('Invalid application MIME subtype') - MIMENonMultipart.__init__(self, 'application', _subtype, **_params) + MIMENonMultipart.__init__(self, 'application', _subtype, policy=policy, + **_params) self.set_payload(_data) _encoder(self) diff --git a/Lib/email/mime/audio.py b/Lib/email/mime/audio.py --- a/Lib/email/mime/audio.py +++ b/Lib/email/mime/audio.py @@ -38,17 +38,17 @@ def _whatsnd(data): return None class MIMEAudio(MIMENonMultipart): """Class for generating audio/* MIME documents.""" def __init__(self, _audiodata, _subtype=None, - _encoder=encoders.encode_base64, **_params): + _encoder=encoders.encode_base64, *, policy=None, **_params): """Create an audio/* type MIME document. _audiodata is a string containing the raw audio data. If this data can be decoded by the standard Python `sndhdr' module, then the subtype will be automatically included in the Content-Type header. Otherwise, you can specify the specific audio subtype via the _subtype parameter. If _subtype is not given, and no subtype can be guessed, a TypeError is raised. @@ -63,11 +63,12 @@ class MIMEAudio(MIMENonMultipart): Any additional keyword arguments are passed to the base class constructor, which turns them into parameters on the Content-Type header. """ if _subtype is None: _subtype = _whatsnd(_audiodata) if _subtype is None: raise TypeError('Could not find audio MIME subtype') - MIMENonMultipart.__init__(self, 'audio', _subtype, **_params) + MIMENonMultipart.__init__(self, 'audio', _subtype, policy=policy, + **_params) self.set_payload(_audiodata) _encoder(self) diff --git a/Lib/email/mime/base.py b/Lib/email/mime/base.py --- a/Lib/email/mime/base.py +++ b/Lib/email/mime/base.py @@ -1,26 +1,30 @@ # Copyright (C) 2001-2006 Python Software Foundation # Author: Barry Warsaw # Contact: email-sig@python.org """Base class for MIME specializations.""" __all__ = ['MIMEBase'] +import email.policy + from email import message class MIMEBase(message.Message): """Base class for MIME specializations.""" - def __init__(self, _maintype, _subtype, **_params): + def __init__(self, _maintype, _subtype, *, policy=None, **_params): """This constructor adds a Content-Type: and a MIME-Version: header. The Content-Type: header is taken from the _maintype and _subtype arguments. Additional parameters for this header are taken from the keyword arguments. """ - message.Message.__init__(self) + if policy is None: + policy = email.policy.compat32 + message.Message.__init__(self, policy=policy) ctype = '%s/%s' % (_maintype, _subtype) self.add_header('Content-Type', ctype, **_params) self['MIME-Version'] = '1.0' diff --git a/Lib/email/mime/image.py b/Lib/email/mime/image.py --- a/Lib/email/mime/image.py +++ b/Lib/email/mime/image.py @@ -12,17 +12,17 @@ from email import encoders from email.mime.nonmultipart import MIMENonMultipart class MIMEImage(MIMENonMultipart): """Class for generating image/* type MIME documents.""" def __init__(self, _imagedata, _subtype=None, - _encoder=encoders.encode_base64, **_params): + _encoder=encoders.encode_base64, *, policy=None, **_params): """Create an image/* type MIME document. _imagedata is a string containing the raw image data. If this data can be decoded by the standard Python `imghdr' module, then the subtype will be automatically included in the Content-Type header. Otherwise, you can specify the specific image subtype via the _subtype parameter. @@ -36,11 +36,12 @@ class MIMEImage(MIMENonMultipart): Any additional keyword arguments are passed to the base class constructor, which turns them into parameters on the Content-Type header. """ if _subtype is None: _subtype = imghdr.what(None, _imagedata) if _subtype is None: raise TypeError('Could not guess image MIME subtype') - MIMENonMultipart.__init__(self, 'image', _subtype, **_params) + MIMENonMultipart.__init__(self, 'image', _subtype, policy=policy, + **_params) self.set_payload(_imagedata) _encoder(self) diff --git a/Lib/email/mime/message.py b/Lib/email/mime/message.py --- a/Lib/email/mime/message.py +++ b/Lib/email/mime/message.py @@ -9,26 +9,26 @@ from email import message from email.mime.nonmultipart import MIMENonMultipart class MIMEMessage(MIMENonMultipart): """Class representing message/* MIME documents.""" - def __init__(self, _msg, _subtype='rfc822'): + def __init__(self, _msg, _subtype='rfc822', *, policy=None): """Create a message/* type MIME document. _msg is a message object and must be an instance of Message, or a derived class of Message, otherwise a TypeError is raised. Optional _subtype defines the subtype of the contained message. The default is "rfc822" (this is defined by the MIME standard, even though the term "rfc822" is technically outdated by RFC 2822). """ - MIMENonMultipart.__init__(self, 'message', _subtype) + MIMENonMultipart.__init__(self, 'message', _subtype, policy=policy) if not isinstance(_msg, message.Message): raise TypeError('Argument is not an instance of Message') # It's convenient to use this base class method. We need to do it # this way or we'll get an exception message.Message.attach(self, _msg) # And be sure our default type is set correctly self.set_default_type('message/rfc822') diff --git a/Lib/email/mime/multipart.py b/Lib/email/mime/multipart.py --- a/Lib/email/mime/multipart.py +++ b/Lib/email/mime/multipart.py @@ -9,16 +9,17 @@ from email.mime.base import MIMEBase class MIMEMultipart(MIMEBase): """Base class for MIME multipart/* type messages.""" def __init__(self, _subtype='mixed', boundary=None, _subparts=None, + *, policy=None, **_params): """Creates a multipart/* type message. By default, creates a multipart/mixed message, with proper Content-Type and MIME-Version headers. _subtype is the subtype of the multipart content type, defaulting to `mixed'. @@ -28,17 +29,17 @@ class MIMEMultipart(MIMEBase): _subparts is a sequence of initial subparts for the payload. It must be an iterable object, such as a list. You can always attach new subparts to the message by using the attach() method. Additional parameters for the Content-Type header are taken from the keyword arguments (or passed into the _params argument). """ - MIMEBase.__init__(self, 'multipart', _subtype, **_params) + MIMEBase.__init__(self, 'multipart', _subtype, policy=policy, **_params) # Initialise _payload to an empty list as the Message superclass's # implementation of is_multipart assumes that _payload is a list for # multipart messages. self._payload = [] if _subparts: for p in _subparts: diff --git a/Lib/email/mime/text.py b/Lib/email/mime/text.py --- a/Lib/email/mime/text.py +++ b/Lib/email/mime/text.py @@ -9,17 +9,17 @@ from email.charset import Charset from email.mime.nonmultipart import MIMENonMultipart class MIMEText(MIMENonMultipart): """Class for generating text/* type MIME documents.""" - def __init__(self, _text, _subtype='plain', _charset=None): + def __init__(self, _text, _subtype='plain', _charset=None, *, policy=None): """Create a text/* type MIME document. _text is the string for this message object. _subtype is the MIME sub content type, defaulting to "plain". _charset is the character set parameter added to the Content-Type header. This defaults to "us-ascii". Note that as a side-effect, the @@ -33,12 +33,12 @@ class MIMEText(MIMENonMultipart): try: _text.encode('us-ascii') _charset = 'us-ascii' except UnicodeEncodeError: _charset = 'utf-8' if isinstance(_charset, Charset): _charset = str(_charset) - MIMENonMultipart.__init__(self, 'text', _subtype, + MIMENonMultipart.__init__(self, 'text', _subtype, policy=policy, **{'charset': _charset}) self.set_payload(_text, _charset) diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -26,16 +26,17 @@ from email.generator import Generator, D from email.message import Message from email.mime.application import MIMEApplication from email.mime.audio import MIMEAudio from email.mime.text import MIMEText from email.mime.image import MIMEImage from email.mime.base import MIMEBase from email.mime.message import MIMEMessage from email.mime.multipart import MIMEMultipart +from email.mime.nonmultipart import MIMENonMultipart from email import utils from email import errors from email import encoders from email import iterators from email import base64mime from email import quoprimime from test.support import unlink, start_threads @@ -2057,17 +2058,23 @@ MIME-Version: 1.0 Content-Type: image/file1.jpg MIME-Version: 1.0 Content-Transfer-Encoding: base64 YXNkZg== --===============0012394164==--""") self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==') - + def test_mimebase_default_policy(self): + m = MIMEBase('multipart', 'mixed') + self.assertIs(m.policy, email.policy.compat32) + + def test_mimebase_custom_policy(self): + m = MIMEBase('multipart', 'mixed', policy=email.policy.default) + self.assertIs(m.policy, email.policy.default) # Test some badly formatted messages class TestNonConformant(TestEmailBase): def test_parse_missing_minor_type(self): eq = self.assertEqual msg = self._msgobj('msg_14.txt') eq(msg.get_content_type(), 'text/plain') @@ -2659,16 +2666,29 @@ message 2 eq(len(msg.get_payload()), 2) eq(msg.get_payload(0), text1) eq(msg.get_payload(1), text2) def test_default_multipart_constructor(self): msg = MIMEMultipart() self.assertTrue(msg.is_multipart()) + def test_multipart_default_policy(self): + msg = MIMEMultipart() + msg['To'] = 'a@b.com' + msg['To'] = 'c@d.com' + self.assertEqual(msg.get_all('to'), ['a@b.com', 'c@d.com']) + + def test_multipart_custom_policy(self): + msg = MIMEMultipart(policy=email.policy.default) + msg['To'] = 'a@b.com' + with self.assertRaises(ValueError) as cm: + msg['To'] = 'c@d.com' + self.assertEqual(str(cm.exception), + 'There may be at most 1 To headers in a message') # A general test of parser->model->generator idempotency. IOW, read a message # in, parse it into a message object tree, then without touching the tree, # regenerate the plain text. The original text and the transformed text # should be identical. Note: that we ignore the Unix-From since that may # contain a changed date. class TestIdempotent(TestEmailBase): @@ -3308,16 +3328,37 @@ multipart/report msgtxt = msgtxt.replace(b'with attachment', b'fo\xf6') msgtxt_nl = msgtxt.replace(b'\r\n', b'\n') msg = email.message_from_bytes(msgtxt_nl) s = BytesIO() g = email.generator.BytesGenerator(s) g.flatten(msg, linesep='\r\n') self.assertEqual(s.getvalue(), msgtxt) + def test_mime_classes_policy_argument(self): + with openfile('audiotest.au', 'rb') as fp: + audiodata = fp.read() + with openfile('PyBanner048.gif', 'rb') as fp: + bindata = fp.read() + classes = [ + (MIMEApplication, ('',)), + (MIMEAudio, (audiodata,)), + (MIMEImage, (bindata,)), + (MIMEMessage, (Message(),)), + (MIMENonMultipart, ('multipart', 'mixed')), + (MIMEText, ('',)), + ] + for cls, constructor in classes: + with self.subTest(cls=cls.__name__, policy='compat32'): + m = cls(*constructor) + self.assertIs(m.policy, email.policy.compat32) + with self.subTest(cls=cls.__name__, policy='default'): + m = cls(*constructor, policy=email.policy.default) + self.assertIs(m.policy, email.policy.default) + # Test the iterator/generators class TestIterators(TestEmailBase): def test_body_line_iterator(self): eq = self.assertEqual neq = self.ndiffAssertEqual # First a simple non-multipart message msg = self._msgobj('msg_01.txt')