plone.rfc822


Nameplone.rfc822 JSON
Version 3.0.1 PyPI version JSON
download
home_pagehttps://pypi.org/project/plone.rfc822
SummaryRFC822 marshalling for zope.schema fields
upload_time2024-01-22 19:54:18
maintainer
docs_urlNone
authorMartin Aspeli and contributors
requires_python>=3.8
licenseBSD
keywords zope schema rfc822
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            Introduction
============

This package provides primitives for turning content objects described by ``zope.schema`` fields into RFC (2)822 style messages.
It utilizes the Python standard library's ``email`` module.

It consists of:

* A marker interface ``IPrimaryField`` which can be used to indicate the primary field of a schema.
  The primary field will be used as the message body.
  If there are more than one field marked as primary, the body is turned in a MIME multipart message.
* An interface ``IFieldMarshaler`` which describes marshalers that convert to and from strings suitable for encoding into an RFC 2822 style message.
  These are multi-adapters on ``(context, field)``.
  ``context`` is the content object and ``field`` is the schema field instance.
* Default implementations of ``IFieldMarshaler`` for the standard fields in the ``zope.schema`` package.
* Helper methods to construct messages from one or more schemata or a list of fields, and to parse a message and update a context object accordingly.

The helper methods are described by ``plone.rfc822.interfaces.IMessageAPI``.
They are importable directly from the ``plone.rfc822`` package::

    def constructMessageFromSchema(context, schema, charset='utf-8'):
        """Convenience method which calls ``constructMessage()`` with all the
        fields, in order, of the given schema interface
        """

    def constructMessageFromSchemata(context, schemata, charset='utf-8'):
        """Convenience method which calls ``constructMessage()`` with all the
        fields, in order, of all the given schemata (a sequence of schema
        interfaces).
        """

    def constructMessage(context, fields, charset='utf-8'):
        """Helper method to construct a message.

        ``context`` is a content object.

        ``fields`` is a sequence of (name, field) pairs for the fields which make
        up the message. This can be obtained from zope.schema.getFieldsInOrder,
        for example.

        ``charset`` is the message charset.

        The message body will be constructed from the primary field, i.e. the
        field which is marked with ``IPrimaryField``. If no such field exists,
        the message will have no body. If multiple fields exist, the message will
        be a multipart message. Otherwise, it will contain a scalar string
        payload.

        A field will be ignored if ``(context, field)`` cannot be multi-adapted
        to ``IFieldMarshaler``, or if the ``marshal()`` method returns None.
        """

    def renderMessage(message, mangleFromHeader=False):
        """Render a message to a string
        """

    def initializeObjectFromSchema(context, schema, message, defaultCharset='utf-8'):
        """Convenience method which calls ``initializeObject()`` with all the
        fields, in order, of the given schema interface
        """

    def initializeObjectFromSchemata(context, schemata, message, defaultCharset='utf-8'):
        """Convenience method which calls ``initializeObject()`` with all the
        fields in order, of all the given schemata (a sequence of schema
        interfaces).
        """

    def initializeObject(context, fields, message, defaultCharset='utf-8'):
        """Initialise an object from a message.

        ``context`` is the content object to initialise.

        ``fields`` is a sequence of (name, field) pairs for the fields which make
        up the message. This can be obtained from zope.schema.getFieldsInOrder,
        for example.

        ``message`` is a ``Message`` object.

        ``defaultCharset`` is the default character set to use.

        If the message is a multipart message, the primary fields will be read
        in order.
        """

The message format used adheres to the following rules:

* All non-primary fields are represented as headers.
  The header name is taken from the field name.
  The value is an encoded string as returned by the ``marshal()`` method of the appropriate ``IFieldMarshal`` multi-adapter.
* If no ``IFieldMarshaler`` adapter can be found, the header is ignored.
* Similarly, if no fields are found for a given header when parsing a message, the header is ignored.
* If there is a single primary field, the message has a string payload, which is the marshalled value of the primary field.
  In this case, the ``Content-Type`` header of the message will be obtained from the primary field's marshaler.
* If there are multiple primary fields, each is encoded into its own message, each with its own ``Content-Type`` header.
  The outer message will have a content type of ``multipart/mixed`` and headers for other fields.
* A ``ValueError`` error is raised if a message is being parsed which has more or fewer parts than there are primary fields.
* Duplicate field names are allowed, and will be encoded as duplicate headers.
  When parsing a message, there needs to be one field per header.
  That is, if a message contains two headers with the name 'foo',
  the list of field name/ instance pairs passed to the ``initializeObject()`` method should contain two pairs with the name 'foo'.
  The first field will be used for the first header value, the second field will be used for the second header value.
  If a third 'foo' header appears, it will be ignored.
* Since message headers are always lowercase, field names will be matched case-insensitively when parsing a message.

Supermodel handler
------------------

If ``plone.supermodel`` is installed, this package  will register a namespace handler for the ``marshal`` namespace, with the URI ``http://namespaces.plone.org/supermodel/marshal``.
This can be used to mark a field as the primary field::

    <model xmlns="http://namespaces.plone.org/supermodel/schema"
           xmlns:marshal="http://namespaces.plone.org/supermodel/marshal">
        <schema>
            <field type="zope.schema.Text" name="test" marshal:primary="true">
                <title>Test field</title>
            </field>
        </schema>
    </model>

``plone.supermodel`` may be installed as a dependency using the extra
``[supermodel]``, but this is probably only useful for running the tests.
If the package is not installed, the handler will not be ignored.

License note
------------

This package is released under the BSD license.
Contributors, please do not add dependencies on GPL code.

Issue tracker
-------------

Please report issues via the `Plone issue tracker`_.

.. _`Plone issue tracker`: https://github.com/plone/plone.rfc822/issues

Support
-------

Dexterity use questions may be answered via `Plone's support channels`_.

.. _`Plone's support channels`: http://plone.org/support

Contributing
------------

Sources are at the `Plone code repository hosted at Github <https://github.com/plone/plone.rfc822>`_.

Contributors please read the document `Process for Plone core's development <https://docs.plone.org/develop/coredev/docs/index.html>`_

Changelog
=========

.. You should *NOT* be adding new change log entries to this file.
   You should create a file in the news directory instead.
   For helpful instructions, please see:
   https://github.com/plone/plone.releaser/blob/master/ADD-A-NEWS-ITEM.rst

.. towncrier release notes start

3.0.1 (2024-01-22)
------------------

Internal:


- Update configuration files.
  [plone devs] (237ff4c8, 6e36bcc4)


3.0.0 (2023-04-26)
------------------

Breaking changes:


- Remove long deprecated `renderMessage` function.
  [@jensens] (1-1)
- Drop python 2.7 compatibility.
  [gforcada] (#1)


Internal:


- Update configuration files.
  [plone devs] (a864b30f)


2.0.2 (2020-04-22)
------------------

Bug fixes:


- Minor packaging updates. (#1)


2.0.1 (2019-05-21)
------------------

Bug fixes:


- Use a better type check in the payload parser.
  [Rotonen] (#7)


2.0.0 (2018-11-04)
------------------

Breaking changes:

- Deprecate ``renderMessage(message)``,
  use stdlibs ``message.as_string()`` from ``email.message.Message`` class instead.
  [jensens]

- Newline handling in MIME-headers: ``\n`` are now escaped explicit.
  This follows RFC2822 section 3.2.2.
  [jensens]

- Drop support of Python 2.6
  [jensens]

New features:

- ``constructMessage`` now handles base64 encoding automatically for all marshallers,
  where ``marshaler.ascii`` is ``False`` and ``marshaler.getContentType`` is ``None``.
  [jensens]

- Support for Python 3+
  Also big code overhaul included.
  [jensens]


1.1.4 (2018-06-20)
------------------

New features:

- Start basic Python 3 support.
  [pbauer, dhavlik]


1.1.3 (2016-08-09)
------------------

Fixes:

- code cleanup: pep8, isort, utf8 headers et al.
  [jensens]

- Use zope.interface decorator.
  [gforcada]


1.1.2 (2016-02-21)
------------------

Fixes:

- Fix test isolation problem.
  [thet]

- Replace deprecated ``zope.testing.doctest`` import with ``doctest`` module from stdlib.
  [thet]


1.1.1 (2015-03-21)
------------------

- Update test to reflect the change in the representation of the model namespaces by adding the 18n xml namespace.
  [sneridagh]

- Make sure the tests do not fail if messages contain a trailing blank line. This fixes test failures on Ubuntu 14.04.
  [timo]


1.1 (2013-08-14)
----------------

- Branch for Plone 4.2/4.3 compatibility changes.
  [esteele]


1.0.2 (2013-07-28)
------------------

- Marshall collections as ASCII when possible.
  [davisagli]

- Add support for marshalling decimal fields.
  [davisagli]

1.0.1 (2013-01-01)
------------------

1.0 (2011-05-20)
----------------

* Relicensed under the BSD license.
  See http://plone.org/foundation/materials/foundation-resolutions/plone-framework-components-relicensing-policy
  [davisagli]

1.0b2 (2011-02-11)
------------------

* Add IPrimaryFieldInfo to look up primary field information on a content item.

1.0b1 (2009-10-08)
------------------

* Initial release

Message construction and parsing
================================

This package contains helper methods to construct an RFC 2822 style message
from a list of schema fields, and to parse a message and initialise an object
based on its headers and body payload.

Before we begin, let's load the default field marshalers and configure
annotations, which we will use later in this test::

    >>> configuration = u"""\
    ... <configure
    ...      xmlns="http://namespaces.zope.org/zope"
    ...      i18n_domain="plone.rfc822.tests">
    ...
    ...     <include package="zope.component" file="meta.zcml" />
    ...     <include package="zope.annotation" />
    ...
    ...     <include package="plone.rfc822" />
    ...
    ... </configure>
    ... """

::

    >>> from io import StringIO
    >>> from zope.configuration import xmlconfig
    >>> xmlconfig.xmlconfig(StringIO(configuration))

The primary field
-----------------

The message body is assumed to originate from a "primary" field, which is
indicated via a marker interface.

To illustrate the pattern, consider the following schema interface::

    >>> from zope.interface import Interface, alsoProvides
    >>> from plone.rfc822.interfaces import IPrimaryField
    >>> from zope import schema

    >>> class ITestContent(Interface):
    ...
    ...     title = schema.TextLine(title=u"Title")
    ...     description = schema.Text(title=u"Description")
    ...     body = schema.Text(title=u"Body text")
    ...     emptyfield = schema.TextLine(title=u"Empty field", missing_value=u'missing')

The primary field instance is marked like this::

    >>> alsoProvides(ITestContent['body'], IPrimaryField)

Constructing a message
----------------------

Let's now say we have an instance providing this interface, which we want to
marshal to a message::

    >>> from zope.interface import implementer
    >>> @implementer(ITestContent)
    ... class TestContent(object):
    ...     title = ""
    ...     description = ""
    ...     body = ""
    ...     emptyfield = None

    >>> content = TestContent()
    >>> content.title = "Test title"
    >>> content.description = """Täst description
    ... with a newline"""
    >>> content.body = "<p>Test body</p>"

We could create a message from this instance and schema like this::

    >>> from plone.rfc822 import constructMessageFromSchema
    >>> msg = constructMessageFromSchema(content, ITestContent)

The output looks like this::

    >>> print(msg.as_string())
    title: Test title
    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=
    emptyfield:
    Content-Type: text/plain; charset="utf-8"
    <BLANKLINE>
    <p>Test body</p>

Notice how the non-ASCII header values are UTF-8 encoded.
The encoding algorithm is clever enough to only encode the value if it is necessary,
leaving more readable field values otherwise.

The body here is of the default message type::

    >>> msg.get_default_type()
    'text/plain'

This is because none of the default field types manage a content type.

The body is also utf-8 encoded, because the primary field specified this
encoding.

If we want to use a different content type, we could set it explicitly::

    >>> msg.set_type('text/html')
    >>> print(msg.as_string())
    title: Test title
    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=
    emptyfield:
    MIME-Version: 1.0
    Content-Type: text/html; charset="utf-8"
    <BLANKLINE>
    <p>Test body</p>

Alternatively, if we know that any ``IText`` field on an object providing
our ``ITestContent`` interface always stores HTML, could register a custom
``IFieldMarshaler`` adapter which would indicate this to the message
constructor. Let's take a look at that now.

Custom marshalers
-----------------

The default marshaler can be obtained by multi-adapting the content object
and the field instance to ``IFieldMarshaler``:

    >>> from zope.component import getMultiAdapter
    >>> from plone.rfc822.interfaces import IFieldMarshaler
    >>> getMultiAdapter((content, ITestContent['body'],), IFieldMarshaler)
    <plone.rfc822.defaultfields.UnicodeValueFieldMarshaler object at ...>

Let's now create our own marshaler by extending this class and overriding
the ``getContentType()``:

    >>> from plone.rfc822.defaultfields import UnicodeValueFieldMarshaler
    >>> from zope.schema.interfaces import IText
    >>> from zope.component import adapter

    >>> @adapter(ITestContent, IText)
    ... class TestBodyMarshaler(UnicodeValueFieldMarshaler):
    ...     def getContentType(self):
    ...         return 'text/html'

Ordinarily, we'd register this with ZCML. For the purpose of the test, we'll
register it using the ``zope.component`` API.

    >>> from zope.component import provideAdapter
    >>> provideAdapter(TestBodyMarshaler)

Hint: If the schema contained multiple text fields, this adapter would apply
to all of them. To avoid that, we could either mark the field with a custom
marker interface (similarly to the way we marked a field with ``IPrimaryField``
above), or have the marshaler check the field name.

Let's now try again:

    >>> msg = constructMessageFromSchema(content, ITestContent)
    >>> print(msg.as_string())
    title: Test title
    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=
    emptyfield:
    MIME-Version: 1.0
    Content-Type: text/html; charset="utf-8"
    <BLANKLINE>
    <p>Test body</p>

Notice how the Content-Type has changed.

Consuming a message
-------------------

A message can be used to initialise an object. The object has to be
constructed first:

    >>> newContent = TestContent()

We then need to obtain a ``Message`` object. The ``email`` module contains
helper functions for this purpose.

    >>> messageBody = """\
    ... title: Test title
    ... description: =?utf-8?q?Test_description=0D=0Awith_a_newline?=
    ... Content-Type: text/html
    ...
    ... <p>Test body</p>"""

    >>> from email import message_from_string
    >>> msg = message_from_string(messageBody)

The message can now be used to initialise the object according to the given
schema. This should be the same schema as the one used to construct the
message.

    >>> from plone.rfc822 import initializeObjectFromSchema
    >>> initializeObjectFromSchema(newContent, ITestContent, msg)

    >>> newContent.title
    'Test title'
    >>> print(newContent.description)
    Test description
    with a newline

    >>> newContent.body
    '<p>Test body</p>'

We can also consume messages with a transfer encoding and a charset:

    >>> messageBody = """\
    ... title: =?utf-8?q?Test_title?=
    ... description: =?utf-8?q?Test_description=0D=0Awith_a_newline?=
    ... emptyfield:
    ... Content-Transfer-Encoding: base64
    ... Content-Type: text/html; charset="utf-8"
    ... <BLANKLINE>
    ... PHA+VGVzdCBib2R5PC9wPg==
    ... <BLANKLINE>"""

    >>> msg = message_from_string(messageBody)
    >>> msg.get_content_type()
    'text/html'
    >>> msg.get_content_charset()
    'utf-8'

    >>> initializeObjectFromSchema(newContent, ITestContent, msg)

    >>> newContent.title
    'Test title'
    >>> print(newContent.description)
    Test description
    with a newline
    >>> newContent.body
    '<p>Test body</p>'

Note: Empty fields will result in the field's ``missing_value`` being used:

    >>> newContent.emptyfield
    'missing'

Handling multiple primary fields and duplicate field names
----------------------------------------------------------

It is possible that our type could have multiple primary fields or even
duplicate field names.

For example, consider the following schema interface, intended to be used
in an annotation adapter:

    >>> class IPersonalDetails(Interface):
    ...     description = schema.Text(title=u"Personal description")
    ...     currentAge = schema.Int(title=u"Age", min=0)
    ...     personalProfile = schema.Text(title=u"Profile")

    >>> alsoProvides(IPersonalDetails['personalProfile'], IPrimaryField)

The annotation storage would look like this:

    >>> from persistent import Persistent
    >>> @implementer(IPersonalDetails)
    ... @adapter(ITestContent)
    ... class PersonalDetailsAnnotation(Persistent):
    ...
    ...     def __init__(self):
    ...         self.description = None
    ...         self.currentAge = None
    ...         self.personalProfile = None

    >>> from zope.annotation.factory import factory
    >>> provideAdapter(factory(PersonalDetailsAnnotation))

We should now be able to adapt a content instance to IPersonalDetails,
provided it is annotatable.

    >>> from zope.annotation.interfaces import IAttributeAnnotatable
    >>> alsoProvides(content, IAttributeAnnotatable)

    >>> personalDetails = IPersonalDetails(content)
    >>> personalDetails.description = u"<p>My description</p>"
    >>> personalDetails.currentAge = 21
    >>> personalDetails.personalProfile = u"<p>My profile</p>"

The default marshalers will attempt to adapt the context to the schema of
a given field before getting or setting a value. If we pass multiple schemata
(or a combined sequence of fields) to the message constructor, it will
handle both duplicate field names (as duplicate headers) and multiple primary
fields (as multipart message attachments).

Here are the fields it will see:

    >>> from zope.schema import getFieldsInOrder
    >>> allFields = getFieldsInOrder(ITestContent) + \
    ...             getFieldsInOrder(IPersonalDetails)

    >>> [f[0] for f in allFields]
    ['title', 'description', 'body', 'emptyfield', 'description', 'currentAge', 'personalProfile']

    >>> [f[0] for f in allFields if IPrimaryField.providedBy(f[1])]
    ['body', 'personalProfile']

Let's now construct a message. Since we now have two fields called
``description``, we will get two headers by that name. Since we have two
primary fields, we will get a multipart message with two attachments::

    >>> from plone.rfc822 import constructMessageFromSchemata
    >>> msg = constructMessageFromSchemata(content, (ITestContent, IPersonalDetails,))
    >>> msgString = msg.as_string()
    >>> print(msgString)
    title: Test title
    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=
    emptyfield:
    description: <p>My description</p>
    currentAge: 21
    MIME-Version: 1.0
    Content-Type: multipart/mixed; boundary="===============...=="
    <BLANKLINE>
    --===============...==
    MIME-Version: 1.0
    Content-Type: text/html; charset="utf-8"
    <BLANKLINE>
    <p>Test body</p>
    --===============...==
    MIME-Version: 1.0
    Content-Type: text/html; charset="utf-8"
    <BLANKLINE>
    <p>My profile</p>
    --===============...==--
    <BLANKLINE>


(Note that we've used ellipses here for the doctest to work with the generated
boundary string).

Notice how both messages have a MIME type of 'text/html' and no charset.
That is because of the custom adapter for ``(ITestContent, IText)`` which we
registered earlier.

We can obviously read this message as well. Note that in this case, the order
of fields passed to ``initializeObject()`` is important, both to determine
which field gets which ``description`` header, and to match the two
attachments to the two primary fields:

    >>> newContent = TestContent()
    >>> alsoProvides(newContent, IAttributeAnnotatable)

    >>> from plone.rfc822 import initializeObjectFromSchemata
    >>> msg = message_from_string(msgString)
    >>> initializeObjectFromSchemata(newContent, [ITestContent, IPersonalDetails], msg)

    >>> newContent.title
    'Test title'

    >>> newContent.marker = True
    >>> newContent.description
    'T\xe4st description\nwith a newline'

    >>> newContent.body
    '<p>Test body</p>'

    >>> newPersonalDetails = IPersonalDetails(newContent)
    >>> newPersonalDetails.description
    '<p>My description</p>'

    >>> newPersonalDetails.currentAge
    21

    >>> newPersonalDetails.personalProfile
    '<p>My profile</p>'

Alternative ways to deal with multiple schemata
-----------------------------------------------

In the example above, we created a single enveloping message with headers
corresponding to the fields in both our schemata, and only the primary fields
separated out into different attached payloads.

An alternative approach would be to separate each schema out into its
own multipart message. To do that, we would simply use the
``constructMessage()`` function multiple times.

    >>> mainMessage = constructMessageFromSchema(content, ITestContent)
    >>> personalDetailsMessage = constructMessageFromSchema(content, IPersonalDetails)

    >>> from email.mime.multipart import MIMEMultipart
    >>> envelope = MIMEMultipart()
    >>> envelope.attach(mainMessage)
    >>> envelope.attach(personalDetailsMessage)

    >>> envelopeString = envelope.as_string()
    >>> print(envelopeString)
    Content-Type: multipart/mixed; boundary="===============...=="
    MIME-Version: 1.0
    <BLANKLINE>
    --===============...==
    title: Test title
    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=
    emptyfield:
    MIME-Version: 1.0
    Content-Type: text/html; charset="utf-8"
    <BLANKLINE>
    <p>Test body</p>
    --===============...==
    description: <p>My description</p>
    currentAge: 21
    MIME-Version: 1.0
    Content-Type: text/html; charset="utf-8"
    <BLANKLINE>
    <p>My profile</p>
    --===============...==--...

Which approach works best will depend largely on the intended recipient of
the message.

Encoding the payload and handling filenames
-------------------------------------------

Finally, let's consider a more complex example, inspired by the field
marshaler in ``plone.namedfile``.

Let's say we have a value type intended to represent a binary file with a
filename and content type:

    >>> from zope.interface import Interface, implementer
    >>> from zope import schema

    >>> class IFileValue(Interface):
    ...     data = schema.Bytes(title=u"Raw data")
    ...     contentType = schema.ASCIILine(title=u"MIME type")
    ...     filename = schema.ASCIILine(title=u"Filename")

    >>> @implementer(IFileValue)
    ... class FileValue(object):
    ...
    ...     def __init__(self, data, contentType, filename):
    ...         self.data = data
    ...         self.contentType = contentType
    ...         self.filename = filename

Suppose we had a custom field type to represent this:

    >>> from zope.schema.interfaces import IObject
    >>> class IFileField(IObject):
    ...     pass

    >>> @implementer(IFileField)
    ... class FileField(schema.Object):
    ...     schema = IFileValue
    ...     def __init__(self, **kw):
    ...         if 'schema' in kw:
    ...             self.schema = kw.pop('schema')
    ...         super(FileField, self).__init__(schema=self.schema, **kw)

We can register a field marshaler for this field which will do the following:

* Insist that the field is only used as a primary field, since it makes
  little sense to encode a binary file in a header.
* Save the filename in a Content-Disposition header.
* Be capable of reading the filename again from this header.
* Encode the payload using base64

    >>> from plone.rfc822.interfaces import IFieldMarshaler
    >>> from email.encoders import encode_base64

    >>> from zope.component import adapter
    >>> from plone.rfc822.defaultfields import BaseFieldMarshaler

    >>> @adapter(Interface, IFileField)
    ... class FileFieldMarshaler(BaseFieldMarshaler):
    ...
    ...     ascii = False
    ...
    ...     def encode(self, value, charset='utf-8', primary=False):
    ...         if not primary:
    ...             raise ValueError("File field cannot be marshaled as a non-primary field")
    ...         if value is None:
    ...             return None
    ...         return value.data
    ...
    ...     def decode(self, value, message=None, charset='utf-8', contentType=None, primary=False):
    ...         filename = None
    ...         # get the filename from the Content-Disposition header if possible
    ...         if primary and message is not None:
    ...             filename = message.get_filename(None)
    ...         return FileValue(value, contentType, filename)
    ...
    ...     def getContentType(self):
    ...         value = self._query()
    ...         if value is None:
    ...             return None
    ...         return value.contentType
    ...
    ...     def getCharset(self, default='utf-8'):
    ...         return None # this is not text data!
    ...
    ...     def postProcessMessage(self, message):
    ...         value = self._query()
    ...         if value is not None:
    ...             filename = value.filename
    ...             if filename:
    ...                 # Add a new header storing the filename if we have one
    ...                 message.add_header('Content-Disposition', 'attachment', filename=filename)

    >>> from zope.component import provideAdapter
    >>> provideAdapter(FileFieldMarshaler)

To illustrate marshaling, let's create a content object that contains two file
fields.

    >>> class IFileContent(Interface):
    ...     file1 = FileField()
    ...     file2 = FileField()

    >>> @implementer(IFileContent)
    ... class FileContent(object):
    ...     file1 = None
    ...     file2 = None

    >>> fileContent = FileContent()
    >>> fileContent.file1 = FileValue('dummy file', 'text/plain', 'dummy1.txt')
    >>> fileContent.file2 = FileValue('<html><body>test</body></html>', 'text/html', 'dummy2.html')

At this point, neither of these fields is marked as a primary field. Let's see
what happens when we attempt to construct a message from this schema.

    >>> from plone.rfc822 import constructMessageFromSchema
    >>> message = constructMessageFromSchema(fileContent, IFileContent)
    >>> print(message.as_string())
    <BLANKLINE>
    <BLANKLINE>

As expected, we got no message headers and no message body. Let's now mark one
field as primary:

    >>> from plone.rfc822.interfaces import IPrimaryField
    >>> from zope.interface import alsoProvides
    >>> alsoProvides(IFileContent['file1'], IPrimaryField)

    >>> message = constructMessageFromSchema(fileContent, IFileContent)
    >>> messageBody = message.as_string()
    >>> print(messageBody)
    MIME-Version: 1.0
    Content-Type: text/plain
    Content-Transfer-Encoding: base64
    Content-Disposition: attachment; filename="dummy1.txt"
    <BLANKLINE>
    ZHVtbXkgZmlsZQ==

Here, we have a base64 encoded payload, a Content-Disposition header, and a
Content-Type header according to the primary field.

We can also reconstruct the object from this message.

    >>> from plone.rfc822 import initializeObjectFromSchema
    >>> from email import message_from_string

    >>> inputMessage = message_from_string(messageBody)
    >>> newFileContent = FileContent()
    >>> initializeObjectFromSchema(newFileContent, IFileContent, inputMessage)

    >>> newFileContent.file1.data
    b'dummy file'
    >>> newFileContent.file1.contentType
    'text/plain'
    >>> newFileContent.file1.filename
    'dummy1.txt'

    >>> newFileContent.file2 is None
    True

Let's now show what would happen if we encoded both files in the message.
In this case, we should get a multipart document with two payloads.

    >>> alsoProvides(IFileContent['file2'], IPrimaryField)
    >>> message = constructMessageFromSchema(fileContent, IFileContent)
    >>> messageBody = message.as_string()
    >>> print(messageBody) # doctest: +ELLIPSIS
    MIME-Version: 1.0
    Content-Type: multipart/mixed; boundary="===============...=="
    <BLANKLINE>
    --===============...==
    MIME-Version: 1.0
    Content-Type: text/plain
    Content-Transfer-Encoding: base64
    Content-Disposition: attachment; filename="dummy1.txt"
    <BLANKLINE>
    ZHVtbXkgZmlsZQ==
    --===============...==
    MIME-Version: 1.0
    Content-Type: text/html
    Content-Transfer-Encoding: base64
    Content-Disposition: attachment; filename="dummy2.html"
    <BLANKLINE>
    PGh0bWw+PGJvZHk+dGVzdDwvYm9keT48L2h0bWw+
    --===============...==--...

And again, we can reconstruct the object, this time with both fields:

    >>> inputMessage = message_from_string(messageBody)
    >>> newFileContent = FileContent()
    >>> initializeObjectFromSchema(newFileContent, IFileContent, inputMessage)

    >>> newFileContent.file1.data
    b'dummy file'
    >>> newFileContent.file1.contentType
    'text/plain'
    >>> newFileContent.file1.filename
    'dummy1.txt'

    >>> newFileContent.file2.data
    b'<html><body>test</body></html>'
    >>> newFileContent.file2.contentType
    'text/html'
    >>> newFileContent.file2.filename
    'dummy2.html'

Specialities between Py2 and Py3
--------------------------------

Test a special behavior which is different between Python 2 and 3 stdlib:
Newline handling in non-utf8 strings.

Python 2.7 ``email.header`` keeps a line with an escaped value,
while Python 3.6 turns it into RFC2047 encoded headers, see https://tools.ietf.org/html/rfc2047.html
Technical both is fine.

::

    >>> content.description = "Test content\nwith newline difference"
    >>> msg = constructMessageFromSchema(content, ITestContent)
    >>> effective_output = msg.as_string()
    >>> effective_output.split('\n')[1]
    'description: =?utf-8?q?Test_content=5Cnwith_newline_difference?='

            

Raw data

            {
    "_id": null,
    "home_page": "https://pypi.org/project/plone.rfc822",
    "name": "plone.rfc822",
    "maintainer": "",
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": "",
    "keywords": "zope schema rfc822",
    "author": "Martin Aspeli and contributors",
    "author_email": "optilude@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/67/9c/3348a3676e54c6a814ab40c990ddbdbcffc5cb74330e38eab6250b42de4a/plone.rfc822-3.0.1.tar.gz",
    "platform": null,
    "description": "Introduction\n============\n\nThis package provides primitives for turning content objects described by ``zope.schema`` fields into RFC (2)822 style messages.\nIt utilizes the Python standard library's ``email`` module.\n\nIt consists of:\n\n* A marker interface ``IPrimaryField`` which can be used to indicate the primary field of a schema.\n  The primary field will be used as the message body.\n  If there are more than one field marked as primary, the body is turned in a MIME multipart message.\n* An interface ``IFieldMarshaler`` which describes marshalers that convert to and from strings suitable for encoding into an RFC 2822 style message.\n  These are multi-adapters on ``(context, field)``.\n  ``context`` is the content object and ``field`` is the schema field instance.\n* Default implementations of ``IFieldMarshaler`` for the standard fields in the ``zope.schema`` package.\n* Helper methods to construct messages from one or more schemata or a list of fields, and to parse a message and update a context object accordingly.\n\nThe helper methods are described by ``plone.rfc822.interfaces.IMessageAPI``.\nThey are importable directly from the ``plone.rfc822`` package::\n\n    def constructMessageFromSchema(context, schema, charset='utf-8'):\n        \"\"\"Convenience method which calls ``constructMessage()`` with all the\n        fields, in order, of the given schema interface\n        \"\"\"\n\n    def constructMessageFromSchemata(context, schemata, charset='utf-8'):\n        \"\"\"Convenience method which calls ``constructMessage()`` with all the\n        fields, in order, of all the given schemata (a sequence of schema\n        interfaces).\n        \"\"\"\n\n    def constructMessage(context, fields, charset='utf-8'):\n        \"\"\"Helper method to construct a message.\n\n        ``context`` is a content object.\n\n        ``fields`` is a sequence of (name, field) pairs for the fields which make\n        up the message. This can be obtained from zope.schema.getFieldsInOrder,\n        for example.\n\n        ``charset`` is the message charset.\n\n        The message body will be constructed from the primary field, i.e. the\n        field which is marked with ``IPrimaryField``. If no such field exists,\n        the message will have no body. If multiple fields exist, the message will\n        be a multipart message. Otherwise, it will contain a scalar string\n        payload.\n\n        A field will be ignored if ``(context, field)`` cannot be multi-adapted\n        to ``IFieldMarshaler``, or if the ``marshal()`` method returns None.\n        \"\"\"\n\n    def renderMessage(message, mangleFromHeader=False):\n        \"\"\"Render a message to a string\n        \"\"\"\n\n    def initializeObjectFromSchema(context, schema, message, defaultCharset='utf-8'):\n        \"\"\"Convenience method which calls ``initializeObject()`` with all the\n        fields, in order, of the given schema interface\n        \"\"\"\n\n    def initializeObjectFromSchemata(context, schemata, message, defaultCharset='utf-8'):\n        \"\"\"Convenience method which calls ``initializeObject()`` with all the\n        fields in order, of all the given schemata (a sequence of schema\n        interfaces).\n        \"\"\"\n\n    def initializeObject(context, fields, message, defaultCharset='utf-8'):\n        \"\"\"Initialise an object from a message.\n\n        ``context`` is the content object to initialise.\n\n        ``fields`` is a sequence of (name, field) pairs for the fields which make\n        up the message. This can be obtained from zope.schema.getFieldsInOrder,\n        for example.\n\n        ``message`` is a ``Message`` object.\n\n        ``defaultCharset`` is the default character set to use.\n\n        If the message is a multipart message, the primary fields will be read\n        in order.\n        \"\"\"\n\nThe message format used adheres to the following rules:\n\n* All non-primary fields are represented as headers.\n  The header name is taken from the field name.\n  The value is an encoded string as returned by the ``marshal()`` method of the appropriate ``IFieldMarshal`` multi-adapter.\n* If no ``IFieldMarshaler`` adapter can be found, the header is ignored.\n* Similarly, if no fields are found for a given header when parsing a message, the header is ignored.\n* If there is a single primary field, the message has a string payload, which is the marshalled value of the primary field.\n  In this case, the ``Content-Type`` header of the message will be obtained from the primary field's marshaler.\n* If there are multiple primary fields, each is encoded into its own message, each with its own ``Content-Type`` header.\n  The outer message will have a content type of ``multipart/mixed`` and headers for other fields.\n* A ``ValueError`` error is raised if a message is being parsed which has more or fewer parts than there are primary fields.\n* Duplicate field names are allowed, and will be encoded as duplicate headers.\n  When parsing a message, there needs to be one field per header.\n  That is, if a message contains two headers with the name 'foo',\n  the list of field name/ instance pairs passed to the ``initializeObject()`` method should contain two pairs with the name 'foo'.\n  The first field will be used for the first header value, the second field will be used for the second header value.\n  If a third 'foo' header appears, it will be ignored.\n* Since message headers are always lowercase, field names will be matched case-insensitively when parsing a message.\n\nSupermodel handler\n------------------\n\nIf ``plone.supermodel`` is installed, this package  will register a namespace handler for the ``marshal`` namespace, with the URI ``http://namespaces.plone.org/supermodel/marshal``.\nThis can be used to mark a field as the primary field::\n\n    <model xmlns=\"http://namespaces.plone.org/supermodel/schema\"\n           xmlns:marshal=\"http://namespaces.plone.org/supermodel/marshal\">\n        <schema>\n            <field type=\"zope.schema.Text\" name=\"test\" marshal:primary=\"true\">\n                <title>Test field</title>\n            </field>\n        </schema>\n    </model>\n\n``plone.supermodel`` may be installed as a dependency using the extra\n``[supermodel]``, but this is probably only useful for running the tests.\nIf the package is not installed, the handler will not be ignored.\n\nLicense note\n------------\n\nThis package is released under the BSD license.\nContributors, please do not add dependencies on GPL code.\n\nIssue tracker\n-------------\n\nPlease report issues via the `Plone issue tracker`_.\n\n.. _`Plone issue tracker`: https://github.com/plone/plone.rfc822/issues\n\nSupport\n-------\n\nDexterity use questions may be answered via `Plone's support channels`_.\n\n.. _`Plone's support channels`: http://plone.org/support\n\nContributing\n------------\n\nSources are at the `Plone code repository hosted at Github <https://github.com/plone/plone.rfc822>`_.\n\nContributors please read the document `Process for Plone core's development <https://docs.plone.org/develop/coredev/docs/index.html>`_\n\nChangelog\n=========\n\n.. You should *NOT* be adding new change log entries to this file.\n   You should create a file in the news directory instead.\n   For helpful instructions, please see:\n   https://github.com/plone/plone.releaser/blob/master/ADD-A-NEWS-ITEM.rst\n\n.. towncrier release notes start\n\n3.0.1 (2024-01-22)\n------------------\n\nInternal:\n\n\n- Update configuration files.\n  [plone devs] (237ff4c8, 6e36bcc4)\n\n\n3.0.0 (2023-04-26)\n------------------\n\nBreaking changes:\n\n\n- Remove long deprecated `renderMessage` function.\n  [@jensens] (1-1)\n- Drop python 2.7 compatibility.\n  [gforcada] (#1)\n\n\nInternal:\n\n\n- Update configuration files.\n  [plone devs] (a864b30f)\n\n\n2.0.2 (2020-04-22)\n------------------\n\nBug fixes:\n\n\n- Minor packaging updates. (#1)\n\n\n2.0.1 (2019-05-21)\n------------------\n\nBug fixes:\n\n\n- Use a better type check in the payload parser.\n  [Rotonen] (#7)\n\n\n2.0.0 (2018-11-04)\n------------------\n\nBreaking changes:\n\n- Deprecate ``renderMessage(message)``,\n  use stdlibs ``message.as_string()`` from ``email.message.Message`` class instead.\n  [jensens]\n\n- Newline handling in MIME-headers: ``\\n`` are now escaped explicit.\n  This follows RFC2822 section 3.2.2.\n  [jensens]\n\n- Drop support of Python 2.6\n  [jensens]\n\nNew features:\n\n- ``constructMessage`` now handles base64 encoding automatically for all marshallers,\n  where ``marshaler.ascii`` is ``False`` and ``marshaler.getContentType`` is ``None``.\n  [jensens]\n\n- Support for Python 3+\n  Also big code overhaul included.\n  [jensens]\n\n\n1.1.4 (2018-06-20)\n------------------\n\nNew features:\n\n- Start basic Python 3 support.\n  [pbauer, dhavlik]\n\n\n1.1.3 (2016-08-09)\n------------------\n\nFixes:\n\n- code cleanup: pep8, isort, utf8 headers et al.\n  [jensens]\n\n- Use zope.interface decorator.\n  [gforcada]\n\n\n1.1.2 (2016-02-21)\n------------------\n\nFixes:\n\n- Fix test isolation problem.\n  [thet]\n\n- Replace deprecated ``zope.testing.doctest`` import with ``doctest`` module from stdlib.\n  [thet]\n\n\n1.1.1 (2015-03-21)\n------------------\n\n- Update test to reflect the change in the representation of the model namespaces by adding the 18n xml namespace.\n  [sneridagh]\n\n- Make sure the tests do not fail if messages contain a trailing blank line. This fixes test failures on Ubuntu 14.04.\n  [timo]\n\n\n1.1 (2013-08-14)\n----------------\n\n- Branch for Plone 4.2/4.3 compatibility changes.\n  [esteele]\n\n\n1.0.2 (2013-07-28)\n------------------\n\n- Marshall collections as ASCII when possible.\n  [davisagli]\n\n- Add support for marshalling decimal fields.\n  [davisagli]\n\n1.0.1 (2013-01-01)\n------------------\n\n1.0 (2011-05-20)\n----------------\n\n* Relicensed under the BSD license.\n  See http://plone.org/foundation/materials/foundation-resolutions/plone-framework-components-relicensing-policy\n  [davisagli]\n\n1.0b2 (2011-02-11)\n------------------\n\n* Add IPrimaryFieldInfo to look up primary field information on a content item.\n\n1.0b1 (2009-10-08)\n------------------\n\n* Initial release\n\nMessage construction and parsing\n================================\n\nThis package contains helper methods to construct an RFC 2822 style message\nfrom a list of schema fields, and to parse a message and initialise an object\nbased on its headers and body payload.\n\nBefore we begin, let's load the default field marshalers and configure\nannotations, which we will use later in this test::\n\n    >>> configuration = u\"\"\"\\\n    ... <configure\n    ...      xmlns=\"http://namespaces.zope.org/zope\"\n    ...      i18n_domain=\"plone.rfc822.tests\">\n    ...\n    ...     <include package=\"zope.component\" file=\"meta.zcml\" />\n    ...     <include package=\"zope.annotation\" />\n    ...\n    ...     <include package=\"plone.rfc822\" />\n    ...\n    ... </configure>\n    ... \"\"\"\n\n::\n\n    >>> from io import StringIO\n    >>> from zope.configuration import xmlconfig\n    >>> xmlconfig.xmlconfig(StringIO(configuration))\n\nThe primary field\n-----------------\n\nThe message body is assumed to originate from a \"primary\" field, which is\nindicated via a marker interface.\n\nTo illustrate the pattern, consider the following schema interface::\n\n    >>> from zope.interface import Interface, alsoProvides\n    >>> from plone.rfc822.interfaces import IPrimaryField\n    >>> from zope import schema\n\n    >>> class ITestContent(Interface):\n    ...\n    ...     title = schema.TextLine(title=u\"Title\")\n    ...     description = schema.Text(title=u\"Description\")\n    ...     body = schema.Text(title=u\"Body text\")\n    ...     emptyfield = schema.TextLine(title=u\"Empty field\", missing_value=u'missing')\n\nThe primary field instance is marked like this::\n\n    >>> alsoProvides(ITestContent['body'], IPrimaryField)\n\nConstructing a message\n----------------------\n\nLet's now say we have an instance providing this interface, which we want to\nmarshal to a message::\n\n    >>> from zope.interface import implementer\n    >>> @implementer(ITestContent)\n    ... class TestContent(object):\n    ...     title = \"\"\n    ...     description = \"\"\n    ...     body = \"\"\n    ...     emptyfield = None\n\n    >>> content = TestContent()\n    >>> content.title = \"Test title\"\n    >>> content.description = \"\"\"T\u00e4st description\n    ... with a newline\"\"\"\n    >>> content.body = \"<p>Test body</p>\"\n\nWe could create a message from this instance and schema like this::\n\n    >>> from plone.rfc822 import constructMessageFromSchema\n    >>> msg = constructMessageFromSchema(content, ITestContent)\n\nThe output looks like this::\n\n    >>> print(msg.as_string())\n    title: Test title\n    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=\n    emptyfield:\n    Content-Type: text/plain; charset=\"utf-8\"\n    <BLANKLINE>\n    <p>Test body</p>\n\nNotice how the non-ASCII header values are UTF-8 encoded.\nThe encoding algorithm is clever enough to only encode the value if it is necessary,\nleaving more readable field values otherwise.\n\nThe body here is of the default message type::\n\n    >>> msg.get_default_type()\n    'text/plain'\n\nThis is because none of the default field types manage a content type.\n\nThe body is also utf-8 encoded, because the primary field specified this\nencoding.\n\nIf we want to use a different content type, we could set it explicitly::\n\n    >>> msg.set_type('text/html')\n    >>> print(msg.as_string())\n    title: Test title\n    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=\n    emptyfield:\n    MIME-Version: 1.0\n    Content-Type: text/html; charset=\"utf-8\"\n    <BLANKLINE>\n    <p>Test body</p>\n\nAlternatively, if we know that any ``IText`` field on an object providing\nour ``ITestContent`` interface always stores HTML, could register a custom\n``IFieldMarshaler`` adapter which would indicate this to the message\nconstructor. Let's take a look at that now.\n\nCustom marshalers\n-----------------\n\nThe default marshaler can be obtained by multi-adapting the content object\nand the field instance to ``IFieldMarshaler``:\n\n    >>> from zope.component import getMultiAdapter\n    >>> from plone.rfc822.interfaces import IFieldMarshaler\n    >>> getMultiAdapter((content, ITestContent['body'],), IFieldMarshaler)\n    <plone.rfc822.defaultfields.UnicodeValueFieldMarshaler object at ...>\n\nLet's now create our own marshaler by extending this class and overriding\nthe ``getContentType()``:\n\n    >>> from plone.rfc822.defaultfields import UnicodeValueFieldMarshaler\n    >>> from zope.schema.interfaces import IText\n    >>> from zope.component import adapter\n\n    >>> @adapter(ITestContent, IText)\n    ... class TestBodyMarshaler(UnicodeValueFieldMarshaler):\n    ...     def getContentType(self):\n    ...         return 'text/html'\n\nOrdinarily, we'd register this with ZCML. For the purpose of the test, we'll\nregister it using the ``zope.component`` API.\n\n    >>> from zope.component import provideAdapter\n    >>> provideAdapter(TestBodyMarshaler)\n\nHint: If the schema contained multiple text fields, this adapter would apply\nto all of them. To avoid that, we could either mark the field with a custom\nmarker interface (similarly to the way we marked a field with ``IPrimaryField``\nabove), or have the marshaler check the field name.\n\nLet's now try again:\n\n    >>> msg = constructMessageFromSchema(content, ITestContent)\n    >>> print(msg.as_string())\n    title: Test title\n    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=\n    emptyfield:\n    MIME-Version: 1.0\n    Content-Type: text/html; charset=\"utf-8\"\n    <BLANKLINE>\n    <p>Test body</p>\n\nNotice how the Content-Type has changed.\n\nConsuming a message\n-------------------\n\nA message can be used to initialise an object. The object has to be\nconstructed first:\n\n    >>> newContent = TestContent()\n\nWe then need to obtain a ``Message`` object. The ``email`` module contains\nhelper functions for this purpose.\n\n    >>> messageBody = \"\"\"\\\n    ... title: Test title\n    ... description: =?utf-8?q?Test_description=0D=0Awith_a_newline?=\n    ... Content-Type: text/html\n    ...\n    ... <p>Test body</p>\"\"\"\n\n    >>> from email import message_from_string\n    >>> msg = message_from_string(messageBody)\n\nThe message can now be used to initialise the object according to the given\nschema. This should be the same schema as the one used to construct the\nmessage.\n\n    >>> from plone.rfc822 import initializeObjectFromSchema\n    >>> initializeObjectFromSchema(newContent, ITestContent, msg)\n\n    >>> newContent.title\n    'Test title'\n    >>> print(newContent.description)\n    Test description\n    with a newline\n\n    >>> newContent.body\n    '<p>Test body</p>'\n\nWe can also consume messages with a transfer encoding and a charset:\n\n    >>> messageBody = \"\"\"\\\n    ... title: =?utf-8?q?Test_title?=\n    ... description: =?utf-8?q?Test_description=0D=0Awith_a_newline?=\n    ... emptyfield:\n    ... Content-Transfer-Encoding: base64\n    ... Content-Type: text/html; charset=\"utf-8\"\n    ... <BLANKLINE>\n    ... PHA+VGVzdCBib2R5PC9wPg==\n    ... <BLANKLINE>\"\"\"\n\n    >>> msg = message_from_string(messageBody)\n    >>> msg.get_content_type()\n    'text/html'\n    >>> msg.get_content_charset()\n    'utf-8'\n\n    >>> initializeObjectFromSchema(newContent, ITestContent, msg)\n\n    >>> newContent.title\n    'Test title'\n    >>> print(newContent.description)\n    Test description\n    with a newline\n    >>> newContent.body\n    '<p>Test body</p>'\n\nNote: Empty fields will result in the field's ``missing_value`` being used:\n\n    >>> newContent.emptyfield\n    'missing'\n\nHandling multiple primary fields and duplicate field names\n----------------------------------------------------------\n\nIt is possible that our type could have multiple primary fields or even\nduplicate field names.\n\nFor example, consider the following schema interface, intended to be used\nin an annotation adapter:\n\n    >>> class IPersonalDetails(Interface):\n    ...     description = schema.Text(title=u\"Personal description\")\n    ...     currentAge = schema.Int(title=u\"Age\", min=0)\n    ...     personalProfile = schema.Text(title=u\"Profile\")\n\n    >>> alsoProvides(IPersonalDetails['personalProfile'], IPrimaryField)\n\nThe annotation storage would look like this:\n\n    >>> from persistent import Persistent\n    >>> @implementer(IPersonalDetails)\n    ... @adapter(ITestContent)\n    ... class PersonalDetailsAnnotation(Persistent):\n    ...\n    ...     def __init__(self):\n    ...         self.description = None\n    ...         self.currentAge = None\n    ...         self.personalProfile = None\n\n    >>> from zope.annotation.factory import factory\n    >>> provideAdapter(factory(PersonalDetailsAnnotation))\n\nWe should now be able to adapt a content instance to IPersonalDetails,\nprovided it is annotatable.\n\n    >>> from zope.annotation.interfaces import IAttributeAnnotatable\n    >>> alsoProvides(content, IAttributeAnnotatable)\n\n    >>> personalDetails = IPersonalDetails(content)\n    >>> personalDetails.description = u\"<p>My description</p>\"\n    >>> personalDetails.currentAge = 21\n    >>> personalDetails.personalProfile = u\"<p>My profile</p>\"\n\nThe default marshalers will attempt to adapt the context to the schema of\na given field before getting or setting a value. If we pass multiple schemata\n(or a combined sequence of fields) to the message constructor, it will\nhandle both duplicate field names (as duplicate headers) and multiple primary\nfields (as multipart message attachments).\n\nHere are the fields it will see:\n\n    >>> from zope.schema import getFieldsInOrder\n    >>> allFields = getFieldsInOrder(ITestContent) + \\\n    ...             getFieldsInOrder(IPersonalDetails)\n\n    >>> [f[0] for f in allFields]\n    ['title', 'description', 'body', 'emptyfield', 'description', 'currentAge', 'personalProfile']\n\n    >>> [f[0] for f in allFields if IPrimaryField.providedBy(f[1])]\n    ['body', 'personalProfile']\n\nLet's now construct a message. Since we now have two fields called\n``description``, we will get two headers by that name. Since we have two\nprimary fields, we will get a multipart message with two attachments::\n\n    >>> from plone.rfc822 import constructMessageFromSchemata\n    >>> msg = constructMessageFromSchemata(content, (ITestContent, IPersonalDetails,))\n    >>> msgString = msg.as_string()\n    >>> print(msgString)\n    title: Test title\n    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=\n    emptyfield:\n    description: <p>My description</p>\n    currentAge: 21\n    MIME-Version: 1.0\n    Content-Type: multipart/mixed; boundary=\"===============...==\"\n    <BLANKLINE>\n    --===============...==\n    MIME-Version: 1.0\n    Content-Type: text/html; charset=\"utf-8\"\n    <BLANKLINE>\n    <p>Test body</p>\n    --===============...==\n    MIME-Version: 1.0\n    Content-Type: text/html; charset=\"utf-8\"\n    <BLANKLINE>\n    <p>My profile</p>\n    --===============...==--\n    <BLANKLINE>\n\n\n(Note that we've used ellipses here for the doctest to work with the generated\nboundary string).\n\nNotice how both messages have a MIME type of 'text/html' and no charset.\nThat is because of the custom adapter for ``(ITestContent, IText)`` which we\nregistered earlier.\n\nWe can obviously read this message as well. Note that in this case, the order\nof fields passed to ``initializeObject()`` is important, both to determine\nwhich field gets which ``description`` header, and to match the two\nattachments to the two primary fields:\n\n    >>> newContent = TestContent()\n    >>> alsoProvides(newContent, IAttributeAnnotatable)\n\n    >>> from plone.rfc822 import initializeObjectFromSchemata\n    >>> msg = message_from_string(msgString)\n    >>> initializeObjectFromSchemata(newContent, [ITestContent, IPersonalDetails], msg)\n\n    >>> newContent.title\n    'Test title'\n\n    >>> newContent.marker = True\n    >>> newContent.description\n    'T\\xe4st description\\nwith a newline'\n\n    >>> newContent.body\n    '<p>Test body</p>'\n\n    >>> newPersonalDetails = IPersonalDetails(newContent)\n    >>> newPersonalDetails.description\n    '<p>My description</p>'\n\n    >>> newPersonalDetails.currentAge\n    21\n\n    >>> newPersonalDetails.personalProfile\n    '<p>My profile</p>'\n\nAlternative ways to deal with multiple schemata\n-----------------------------------------------\n\nIn the example above, we created a single enveloping message with headers\ncorresponding to the fields in both our schemata, and only the primary fields\nseparated out into different attached payloads.\n\nAn alternative approach would be to separate each schema out into its\nown multipart message. To do that, we would simply use the\n``constructMessage()`` function multiple times.\n\n    >>> mainMessage = constructMessageFromSchema(content, ITestContent)\n    >>> personalDetailsMessage = constructMessageFromSchema(content, IPersonalDetails)\n\n    >>> from email.mime.multipart import MIMEMultipart\n    >>> envelope = MIMEMultipart()\n    >>> envelope.attach(mainMessage)\n    >>> envelope.attach(personalDetailsMessage)\n\n    >>> envelopeString = envelope.as_string()\n    >>> print(envelopeString)\n    Content-Type: multipart/mixed; boundary=\"===============...==\"\n    MIME-Version: 1.0\n    <BLANKLINE>\n    --===============...==\n    title: Test title\n    description: =?utf-8?q?T=C3=A4st_description=5Cnwith_a_newline?=\n    emptyfield:\n    MIME-Version: 1.0\n    Content-Type: text/html; charset=\"utf-8\"\n    <BLANKLINE>\n    <p>Test body</p>\n    --===============...==\n    description: <p>My description</p>\n    currentAge: 21\n    MIME-Version: 1.0\n    Content-Type: text/html; charset=\"utf-8\"\n    <BLANKLINE>\n    <p>My profile</p>\n    --===============...==--...\n\nWhich approach works best will depend largely on the intended recipient of\nthe message.\n\nEncoding the payload and handling filenames\n-------------------------------------------\n\nFinally, let's consider a more complex example, inspired by the field\nmarshaler in ``plone.namedfile``.\n\nLet's say we have a value type intended to represent a binary file with a\nfilename and content type:\n\n    >>> from zope.interface import Interface, implementer\n    >>> from zope import schema\n\n    >>> class IFileValue(Interface):\n    ...     data = schema.Bytes(title=u\"Raw data\")\n    ...     contentType = schema.ASCIILine(title=u\"MIME type\")\n    ...     filename = schema.ASCIILine(title=u\"Filename\")\n\n    >>> @implementer(IFileValue)\n    ... class FileValue(object):\n    ...\n    ...     def __init__(self, data, contentType, filename):\n    ...         self.data = data\n    ...         self.contentType = contentType\n    ...         self.filename = filename\n\nSuppose we had a custom field type to represent this:\n\n    >>> from zope.schema.interfaces import IObject\n    >>> class IFileField(IObject):\n    ...     pass\n\n    >>> @implementer(IFileField)\n    ... class FileField(schema.Object):\n    ...     schema = IFileValue\n    ...     def __init__(self, **kw):\n    ...         if 'schema' in kw:\n    ...             self.schema = kw.pop('schema')\n    ...         super(FileField, self).__init__(schema=self.schema, **kw)\n\nWe can register a field marshaler for this field which will do the following:\n\n* Insist that the field is only used as a primary field, since it makes\n  little sense to encode a binary file in a header.\n* Save the filename in a Content-Disposition header.\n* Be capable of reading the filename again from this header.\n* Encode the payload using base64\n\n    >>> from plone.rfc822.interfaces import IFieldMarshaler\n    >>> from email.encoders import encode_base64\n\n    >>> from zope.component import adapter\n    >>> from plone.rfc822.defaultfields import BaseFieldMarshaler\n\n    >>> @adapter(Interface, IFileField)\n    ... class FileFieldMarshaler(BaseFieldMarshaler):\n    ...\n    ...     ascii = False\n    ...\n    ...     def encode(self, value, charset='utf-8', primary=False):\n    ...         if not primary:\n    ...             raise ValueError(\"File field cannot be marshaled as a non-primary field\")\n    ...         if value is None:\n    ...             return None\n    ...         return value.data\n    ...\n    ...     def decode(self, value, message=None, charset='utf-8', contentType=None, primary=False):\n    ...         filename = None\n    ...         # get the filename from the Content-Disposition header if possible\n    ...         if primary and message is not None:\n    ...             filename = message.get_filename(None)\n    ...         return FileValue(value, contentType, filename)\n    ...\n    ...     def getContentType(self):\n    ...         value = self._query()\n    ...         if value is None:\n    ...             return None\n    ...         return value.contentType\n    ...\n    ...     def getCharset(self, default='utf-8'):\n    ...         return None # this is not text data!\n    ...\n    ...     def postProcessMessage(self, message):\n    ...         value = self._query()\n    ...         if value is not None:\n    ...             filename = value.filename\n    ...             if filename:\n    ...                 # Add a new header storing the filename if we have one\n    ...                 message.add_header('Content-Disposition', 'attachment', filename=filename)\n\n    >>> from zope.component import provideAdapter\n    >>> provideAdapter(FileFieldMarshaler)\n\nTo illustrate marshaling, let's create a content object that contains two file\nfields.\n\n    >>> class IFileContent(Interface):\n    ...     file1 = FileField()\n    ...     file2 = FileField()\n\n    >>> @implementer(IFileContent)\n    ... class FileContent(object):\n    ...     file1 = None\n    ...     file2 = None\n\n    >>> fileContent = FileContent()\n    >>> fileContent.file1 = FileValue('dummy file', 'text/plain', 'dummy1.txt')\n    >>> fileContent.file2 = FileValue('<html><body>test</body></html>', 'text/html', 'dummy2.html')\n\nAt this point, neither of these fields is marked as a primary field. Let's see\nwhat happens when we attempt to construct a message from this schema.\n\n    >>> from plone.rfc822 import constructMessageFromSchema\n    >>> message = constructMessageFromSchema(fileContent, IFileContent)\n    >>> print(message.as_string())\n    <BLANKLINE>\n    <BLANKLINE>\n\nAs expected, we got no message headers and no message body. Let's now mark one\nfield as primary:\n\n    >>> from plone.rfc822.interfaces import IPrimaryField\n    >>> from zope.interface import alsoProvides\n    >>> alsoProvides(IFileContent['file1'], IPrimaryField)\n\n    >>> message = constructMessageFromSchema(fileContent, IFileContent)\n    >>> messageBody = message.as_string()\n    >>> print(messageBody)\n    MIME-Version: 1.0\n    Content-Type: text/plain\n    Content-Transfer-Encoding: base64\n    Content-Disposition: attachment; filename=\"dummy1.txt\"\n    <BLANKLINE>\n    ZHVtbXkgZmlsZQ==\n\nHere, we have a base64 encoded payload, a Content-Disposition header, and a\nContent-Type header according to the primary field.\n\nWe can also reconstruct the object from this message.\n\n    >>> from plone.rfc822 import initializeObjectFromSchema\n    >>> from email import message_from_string\n\n    >>> inputMessage = message_from_string(messageBody)\n    >>> newFileContent = FileContent()\n    >>> initializeObjectFromSchema(newFileContent, IFileContent, inputMessage)\n\n    >>> newFileContent.file1.data\n    b'dummy file'\n    >>> newFileContent.file1.contentType\n    'text/plain'\n    >>> newFileContent.file1.filename\n    'dummy1.txt'\n\n    >>> newFileContent.file2 is None\n    True\n\nLet's now show what would happen if we encoded both files in the message.\nIn this case, we should get a multipart document with two payloads.\n\n    >>> alsoProvides(IFileContent['file2'], IPrimaryField)\n    >>> message = constructMessageFromSchema(fileContent, IFileContent)\n    >>> messageBody = message.as_string()\n    >>> print(messageBody) # doctest: +ELLIPSIS\n    MIME-Version: 1.0\n    Content-Type: multipart/mixed; boundary=\"===============...==\"\n    <BLANKLINE>\n    --===============...==\n    MIME-Version: 1.0\n    Content-Type: text/plain\n    Content-Transfer-Encoding: base64\n    Content-Disposition: attachment; filename=\"dummy1.txt\"\n    <BLANKLINE>\n    ZHVtbXkgZmlsZQ==\n    --===============...==\n    MIME-Version: 1.0\n    Content-Type: text/html\n    Content-Transfer-Encoding: base64\n    Content-Disposition: attachment; filename=\"dummy2.html\"\n    <BLANKLINE>\n    PGh0bWw+PGJvZHk+dGVzdDwvYm9keT48L2h0bWw+\n    --===============...==--...\n\nAnd again, we can reconstruct the object, this time with both fields:\n\n    >>> inputMessage = message_from_string(messageBody)\n    >>> newFileContent = FileContent()\n    >>> initializeObjectFromSchema(newFileContent, IFileContent, inputMessage)\n\n    >>> newFileContent.file1.data\n    b'dummy file'\n    >>> newFileContent.file1.contentType\n    'text/plain'\n    >>> newFileContent.file1.filename\n    'dummy1.txt'\n\n    >>> newFileContent.file2.data\n    b'<html><body>test</body></html>'\n    >>> newFileContent.file2.contentType\n    'text/html'\n    >>> newFileContent.file2.filename\n    'dummy2.html'\n\nSpecialities between Py2 and Py3\n--------------------------------\n\nTest a special behavior which is different between Python 2 and 3 stdlib:\nNewline handling in non-utf8 strings.\n\nPython 2.7 ``email.header`` keeps a line with an escaped value,\nwhile Python 3.6 turns it into RFC2047 encoded headers, see https://tools.ietf.org/html/rfc2047.html\nTechnical both is fine.\n\n::\n\n    >>> content.description = \"Test content\\nwith newline difference\"\n    >>> msg = constructMessageFromSchema(content, ITestContent)\n    >>> effective_output = msg.as_string()\n    >>> effective_output.split('\\n')[1]\n    'description: =?utf-8?q?Test_content=5Cnwith_newline_difference?='\n",
    "bugtrack_url": null,
    "license": "BSD",
    "summary": "RFC822 marshalling for zope.schema fields",
    "version": "3.0.1",
    "project_urls": {
        "Homepage": "https://pypi.org/project/plone.rfc822"
    },
    "split_keywords": [
        "zope",
        "schema",
        "rfc822"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "bbc2f1ef403b42c16f4a3c108644e61864dc389ab0b054d085d85336ae82719d",
                "md5": "37feccbde40059e91d1a69979ed17d6d",
                "sha256": "a1d832646e7a20c968205550765dbe86c45b6e04c7885fe8e48819ae52b547af"
            },
            "downloads": -1,
            "filename": "plone.rfc822-3.0.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "37feccbde40059e91d1a69979ed17d6d",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 29196,
            "upload_time": "2024-01-22T19:54:16",
            "upload_time_iso_8601": "2024-01-22T19:54:16.885638Z",
            "url": "https://files.pythonhosted.org/packages/bb/c2/f1ef403b42c16f4a3c108644e61864dc389ab0b054d085d85336ae82719d/plone.rfc822-3.0.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "679c3348a3676e54c6a814ab40c990ddbdbcffc5cb74330e38eab6250b42de4a",
                "md5": "63aa683c4903610d9a90a77308b2d781",
                "sha256": "2a4a58693203351f764293142bdecc6ccd203ad9a660b90fb7f001185a0185ac"
            },
            "downloads": -1,
            "filename": "plone.rfc822-3.0.1.tar.gz",
            "has_sig": false,
            "md5_digest": "63aa683c4903610d9a90a77308b2d781",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 38437,
            "upload_time": "2024-01-22T19:54:18",
            "upload_time_iso_8601": "2024-01-22T19:54:18.984976Z",
            "url": "https://files.pythonhosted.org/packages/67/9c/3348a3676e54c6a814ab40c990ddbdbcffc5cb74330e38eab6250b42de4a/plone.rfc822-3.0.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-01-22 19:54:18",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "plone.rfc822"
}
        
Elapsed time: 0.16795s