Rev 2640: (Adeodato Simó) EmailMessage class, allowing much nicer access to Email object than stdlib in http://bzr.arbash-meinel.com/branches/bzr/jam-integration

John Arbash Meinel john at arbash-meinel.com
Fri Jul 20 15:29:14 BST 2007


At http://bzr.arbash-meinel.com/branches/bzr/jam-integration

------------------------------------------------------------
revno: 2640
revision-id: john at arbash-meinel.com-20070720142859-a24s0khul0yw91bh
parent: pqm at pqm.ubuntu.com-20070720133143-r74lo566tluurmfp
parent: dato at net.com.org.es-20070719212253-iaqokj16w681svwe
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: jam-integration
timestamp: Fri 2007-07-20 09:28:59 -0500
message:
  (Adeodato Simó) EmailMessage class, allowing much nicer access to Email object than stdlib
added:
  bzrlib/email_message.py        email_message.py-20070718143823-660zfcl54xi1v65u-1
  bzrlib/tests/test_email_message.py test_email_message.p-20070718143823-660zfcl54xi1v65u-2
modified:
  NEWS                           NEWS-20050323055033-4e00b5db738777ff
  bzrlib/merge_directive.py      merge_directive.py-20070228184838-ja62280spt1g7f4x-1
  bzrlib/smtp_connection.py      smtp_connection.py-20070618204456-nu6wag1ste4biuk2-1
  bzrlib/tests/__init__.py       selftest.py-20050531073622-8d0e3c8845c97a64
  bzrlib/tests/blackbox/test_merge_directive.py test_merge_directive-20070302012039-zh7uhy39biairtn0-1
  bzrlib/tests/test_merge_directive.py test_merge_directive-20070228184838-ja62280spt1g7f4x-2
  bzrlib/tests/test_smtp_connection.py test_smtp_connection-20070618204509-wuyxc0r0ztrecv7e-1
    ------------------------------------------------------------
    revno: 2625.6.3
    revision-id: dato at net.com.org.es-20070719212253-iaqokj16w681svwe
    parent: dato at net.com.org.es-20070719173843-m4080de2g08tt3th
    committer: Adeodato Simó <dato at net.com.org.es>
    branch nick: bzr.email_message
    timestamp: Thu 2007-07-19 23:22:53 +0200
    message:
      Changes after review by John.
      
      * bzrlib/email_message.py:
        (EmailMessage): inherit from object.
        (EmailMessage.__getitem__): fix grammar in docstring.
      
      * bzrlib/email_message.py,
        bzrlib/tests/test_email_message.py:
        Do not use variables named "string".
      
      * bzrlib/tests/test_email_message.py:
        (TestEmailMessage.test_send): do not assert inside the verify_*
        functions that replace SMTPConnection.send_email. Instead, append the
        received message to a local variable, and call assert in the main body
        function.
    modified:
      bzrlib/email_message.py        email_message.py-20070718143823-660zfcl54xi1v65u-1
      bzrlib/tests/test_email_message.py test_email_message.p-20070718143823-660zfcl54xi1v65u-2
    ------------------------------------------------------------
    revno: 2625.6.2
    revision-id: dato at net.com.org.es-20070719173843-m4080de2g08tt3th
    parent: dato at net.com.org.es-20070718155152-pv6rwq41eckqyxem
    parent: pqm at pqm.ubuntu.com-20070719160934-d51fyijw69oto88p
    committer: Adeodato Simó <dato at net.com.org.es>
    branch nick: bzr.email_message
    timestamp: Thu 2007-07-19 19:38:43 +0200
    message:
      Merge bzr.dev, resolving conflicts and updating test_merge_directive.py.
    removed:
      bzrlib/bundle/common.py        common.py-20050619223838-f25048f6638f04c6
      bzrlib/bundle/old/             old-20051119041827-8f2417a9cc3b67f2
      bzrlib/bundle/old/send_changeset.py send_changeset.py-20050628200204-9478d383946f1871
    added:
      bzrlib/bundle/serializer/v4.py v10.py-20070611062757-5ggj7k18s9dej0fr-1
      bzrlib/multiparent.py          __init__.py-20070410133617-n1jdhcc1n1mibarp-1
      bzrlib/plugins/multiparent.py  mpregen-20070411063203-5x9z7i73add0d6f6-1
      bzrlib/tests/test_multiparent.py test_multiparent.py-20070410133617-n1jdhcc1n1mibarp-4
      doc/developers/bundle-format4.txt bundleformat4.txt-20070621120628-r3332ovd8u4agv8i-1
    renamed:
      bzrlib/tests/blackbox/test_bundle.py => bzrlib/tests/blackbox/test_submit.py test_bundle.py-20060616222707-c21c8b7ea5ef57b1
    modified:
      NEWS                           NEWS-20050323055033-4e00b5db738777ff
      bzrlib/builtins.py             builtins.py-20050830033751-fc01482b9ca23183
      bzrlib/bundle/apply_bundle.py  apply_changeset.py-20050620044656-dba4eb8021a36f95
      bzrlib/bundle/bundle_data.py   read_changeset.py-20050619171944-c0d95aa685537640
      bzrlib/bundle/commands.py      __init__.py-20050617152058-1b6530d9ab85c11c
      bzrlib/bundle/serializer/__init__.py __init__.py-20051118175413-86b97db0b618feef
      bzrlib/bundle/serializer/v08.py v06.py-20051119041339-ee43f97270b01823
      bzrlib/bundle/serializer/v09.py v09.py-20060921014829-2l5elu11mu2ubvek-1
      bzrlib/errors.py               errors.py-20050309040759-20512168c4e14fbd
      bzrlib/fetch.py                fetch.py-20050818234941-26fea6105696365d
      bzrlib/graph.py                graph_walker.py-20070525030359-y852guab65d4wtn0-1
      bzrlib/knit.py                 knit.py-20051212171256-f056ac8f0fbe1bd9
      bzrlib/log.py                  log.py-20050505065812-c40ce11702fe5fb1
      bzrlib/merge.py                merge.py-20050513021216-953b65a438527106
      bzrlib/merge_directive.py      merge_directive.py-20070228184838-ja62280spt1g7f4x-1
      bzrlib/remote.py               remote.py-20060720103555-yeeg2x51vn0rbtdp-1
      bzrlib/repository.py           rev_storage.py-20051111201905-119e9401e46257e3
      bzrlib/tests/__init__.py       selftest.py-20050531073622-8d0e3c8845c97a64
      bzrlib/tests/blackbox/__init__.py __init__.py-20051128053524-eba30d8255e08dc3
      bzrlib/tests/blackbox/test_log.py test_log.py-20060112090212-78f6ea560c868e24
      bzrlib/tests/blackbox/test_merge.py test_merge.py-20060323225809-9bc0459c19917f41
      bzrlib/tests/blackbox/test_merge_directive.py test_merge_directive-20070302012039-zh7uhy39biairtn0-1
      bzrlib/tests/repository_implementations/test_repository.py test_repository.py-20060131092128-ad07f494f5c9d26c
      bzrlib/tests/test_bundle.py    test.py-20050630184834-092aa401ab9f039c
      bzrlib/tests/test_graph.py     test_graph_walker.py-20070525030405-enq4r60hhi9xrujc-1
      bzrlib/tests/test_knit.py      test_knit.py-20051212171302-95d4c00dd5f11f2b
      bzrlib/tests/test_merge_directive.py test_merge_directive-20070228184838-ja62280spt1g7f4x-2
      bzrlib/tests/test_read_bundle.py test_read_bundle.py-20060615211421-ud8cwr1ulgd914zf-1
      bzrlib/tests/test_versionedfile.py test_versionedfile.py-20060222045249-db45c9ed14a1c2e5
      bzrlib/tests/test_xml.py       test_xml.py-20050905091053-80b45588931a9b35
      bzrlib/versionedfile.py        versionedfile.py-20060222045106-5039c71ee3b65490
      bzrlib/weave.py                knit.py-20050627021749-759c29984154256b
      bzrlib/xml5.py                 xml5.py-20050907032657-aac8f960815b66b1
      bzrlib/xml_serializer.py       xml.py-20050309040759-57d51586fdec365d
      doc/developers/bundles.txt     bundles.txt-20070621030528-qkjnugd7iyud6ow3-1
      bzrlib/tests/blackbox/test_submit.py test_bundle.py-20060616222707-c21c8b7ea5ef57b1
    ------------------------------------------------------------
    revno: 2625.6.1
    revision-id: dato at net.com.org.es-20070718155152-pv6rwq41eckqyxem
    parent: pqm at pqm.ubuntu.com-20070717180333-5smmeduk2q3sbzvw
    committer: Adeodato Simó <dato at net.com.org.es>
    branch nick: bzr.email_message
    timestamp: Wed 2007-07-18 17:51:52 +0200
    message:
      New EmailMessage class, façade around email.Message and MIMEMultipart.
      
      * bzrlib/email_message.py,
        bzrlib/tests/test_email_message.py:
        New files.
      
      * bzrlib/tests/__init__.py:
        (test_suite): add bzrlib.tests.test_email_message.
      
      * bzrlib/merge_directive.py:
        (MergeDirective.to_email): Use EmailMessage instead of email.Message.
      
      * bzrlib/tests/test_merge_directive.py,
        bzrlib/tests/blackbox/test_merge_directive.py:
        (__main__): adjust EMAIL1 and EMAIL2 strings to how EmailMessage
        formats itself.
      
      * bzrlib/smtp_connection.py:
        (SMTPConnection.get_message_addresses): do not use methods present in
        email.Message but not in EmailMessage (get_all). Use get() instead of
        __getitem__ to make explicit that None is returned if the header does
        not exist.
      
      * bzrlib/tests/test_smtp_connection.py:
        (TestSMTPConnection.test_get_message_addresses, 
         TestSMTPConnection.test_destination_address_required): test the
         functions against EmailMessage in addition to email.Message.
      
      * NEWS:
        Mention EmailMessage in INTERNALS.
    added:
      bzrlib/email_message.py        email_message.py-20070718143823-660zfcl54xi1v65u-1
      bzrlib/tests/test_email_message.py test_email_message.p-20070718143823-660zfcl54xi1v65u-2
    modified:
      NEWS                           NEWS-20050323055033-4e00b5db738777ff
      bzrlib/merge_directive.py      merge_directive.py-20070228184838-ja62280spt1g7f4x-1
      bzrlib/smtp_connection.py      smtp_connection.py-20070618204456-nu6wag1ste4biuk2-1
      bzrlib/tests/__init__.py       selftest.py-20050531073622-8d0e3c8845c97a64
      bzrlib/tests/blackbox/test_merge_directive.py test_merge_directive-20070302012039-zh7uhy39biairtn0-1
      bzrlib/tests/test_merge_directive.py test_merge_directive-20070228184838-ja62280spt1g7f4x-2
      bzrlib/tests/test_smtp_connection.py test_smtp_connection-20070618204509-wuyxc0r0ztrecv7e-1
-------------- next part --------------
=== added file 'bzrlib/email_message.py'
--- a/bzrlib/email_message.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/email_message.py	2007-07-19 21:22:53 +0000
@@ -0,0 +1,212 @@
+# Copyright (C) 2007 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+"""A convenience class around email.Message and email.MIMEMultipart."""
+
+from email import (
+    Header,
+    Message,
+    MIMEMultipart,
+    MIMEText,
+    Utils,
+    )
+
+from bzrlib import __version__ as _bzrlib_version
+from bzrlib.osutils import safe_unicode
+from bzrlib.smtp_connection import SMTPConnection
+
+
+class EmailMessage(object):
+    """An email message.
+    
+    The constructor needs an origin address, a destination address or addresses
+    and a subject, and accepts a body as well. Add additional parts to the
+    message with add_inline_attachment(). Retrieve the entire formatted message
+    with as_string().
+
+    Headers can be accessed with get() and msg[], and modified with msg[] =.
+    """
+
+    def __init__(self, from_address, to_address, subject, body=None):
+        """Create an email message.
+
+        :param from_address: The origin address, to be put on the From header.
+        :param to_address: The destination address of the message, to be put in
+            the To header. Can also be a list of addresses.
+        :param subject: The subject of the message.
+        :param body: If given, the body of the message.
+
+        All four parameters can be unicode strings or byte strings, but for the
+        addresses and subject byte strings must be encoded in UTF-8. For the
+        body any byte string will be accepted; if it's not ASCII or UTF-8,
+        it'll be sent with charset=8-bit.
+        """
+        self._headers = {}
+        self._body = body
+        self._parts = []
+        self._msgobj = None
+
+        if isinstance(to_address, basestring):
+            to_address = [ to_address ]
+
+        to_addresses = []
+
+        for addr in to_address:
+            to_addresses.append(self.address_to_encoded_header(addr))
+
+        self._headers['To'] = ', '.join(to_addresses)
+        self._headers['From'] = self.address_to_encoded_header(from_address)
+        self._headers['Subject'] = Header.Header(safe_unicode(subject))
+        self._headers['User-Agent'] = 'Bazaar (%s)' % _bzrlib_version
+
+    def add_inline_attachment(self, body, filename=None, mime_subtype='plain'):
+        """Add an inline attachment to the message.
+
+        :param body: A text to attach. Can be an unicode string or a byte
+            string, and it'll be sent as ascii, utf-8, or 8-bit, in that
+            preferred order.
+        :param filename: The name for the attachment. This will give a default
+            name for email programs to save the attachment.
+        :param mime_subtype: MIME subtype of the attachment (eg. 'plain' for
+            text/plain [default]).
+
+        The attachment body will be displayed inline, so do not use this
+        function to attach binary attachments.
+        """
+        # add_inline_attachment() has been called, so the message will be a
+        # MIMEMultipart; add the provided body, if any, as the first attachment
+        if self._body is not None:
+            self._parts.append((self._body, None, 'plain'))
+            self._body = None
+
+        self._parts.append((body, filename, mime_subtype))
+        self._msgobj = None
+
+    def as_string(self, boundary=None):
+        """Return the entire formatted message as a string.
+        
+        :param boundary: The boundary to use between MIME parts, if applicable.
+            Used for tests.
+        """
+        if self._msgobj is not None:
+            return self._msgobj.as_string()
+
+        if not self._parts:
+            self._msgobj = Message.Message()
+            if self._body is not None:
+                body, encoding = self.string_with_encoding(self._body)
+                self._msgobj.set_payload(body, encoding)
+        else:
+            self._msgobj = MIMEMultipart.MIMEMultipart()
+
+            if boundary is not None:
+                self._msgobj.set_boundary(boundary)
+
+            for body, filename, mime_subtype in self._parts:
+                body, encoding = self.string_with_encoding(body)
+                payload = MIMEText.MIMEText(body, mime_subtype, encoding)
+
+                if filename is not None:
+                    content_type = payload['Content-Type']
+                    content_type += '; name="%s"' % filename
+                    payload.replace_header('Content-Type', content_type)
+
+                payload['Content-Disposition'] = 'inline'
+                self._msgobj.attach(payload)
+
+        # sort headers here to ease testing
+        for header, value in sorted(self._headers.items()):
+            self._msgobj[header] = value
+
+        return self._msgobj.as_string()
+
+    __str__ = as_string
+
+    def get(self, header, failobj=None):
+        """Get a header from the message, returning failobj if not present."""
+        return self._headers.get(header, failobj)
+
+    def __getitem__(self, header):
+        """Get a header from the message, returning None if not present.
+        
+        This method intentionally does not raise KeyError to mimic the behavior
+        of __getitem__ in email.Message.
+        """
+        return self._headers.get(header, None)
+
+    def __setitem__(self, header, value):
+        return self._headers.__setitem__(header, value)
+
+    @staticmethod
+    def send(config, from_address, to_address, subject, body, attachment=None,
+            attachment_filename=None, attachment_mime_subtype='plain'):
+        """Create an email message and send it with SMTPConnection.
+
+        :param config: config object to pass to SMTPConnection constructor.
+
+        See EmailMessage.__init__() and EmailMessage.add_inline_attachment()
+        for an explanation of the rest of parameters.
+        """
+        msg = EmailMessage(from_address, to_address, subject, body)
+        if attachment is not None:
+            msg.add_inline_attachment(attachment, attachment_filename,
+                    attachment_mime_subtype)
+        SMTPConnection(config).send_email(msg)
+
+    @staticmethod
+    def address_to_encoded_header(address):
+        """RFC2047-encode an address if necessary.
+
+        :param address: An unicode string, or UTF-8 byte string.
+        :return: A possibly RFC2047-encoded string.
+        """
+        # Can't call Header over all the address, because that encodes both the
+        # name and the email address, which is not permitted by RFCs.
+        user, email = Utils.parseaddr(address)
+        if not user:
+            return email
+        else:
+            return Utils.formataddr((str(Header.Header(safe_unicode(user))),
+                email))
+
+    @staticmethod
+    def string_with_encoding(string_):
+        """Return a str object together with an encoding.
+
+        :param string_: A str or unicode object.
+        :return: A tuple (str, encoding), where encoding is one of 'ascii',
+            'utf-8', or '8-bit', in that preferred order.
+        """
+        # Python's email module base64-encodes the body whenever the charset is
+        # not explicitly set to ascii. Because of this, and because we want to
+        # avoid base64 when it's not necessary in order to be most compatible
+        # with the capabilities of the receiving side, we check with encode()
+        # and decode() whether the body is actually ascii-only.
+        if isinstance(string_, unicode):
+            try:
+                return (string_.encode('ascii'), 'ascii')
+            except UnicodeEncodeError:
+                return (string_.encode('utf-8'), 'utf-8')
+        else:
+            try:
+                string_.decode('ascii')
+                return (string_, 'ascii')
+            except UnicodeDecodeError:
+                try:
+                    string_.decode('utf-8')
+                    return (string_, 'utf-8')
+                except UnicodeDecodeError:
+                    return (string_, '8-bit')

=== added file 'bzrlib/tests/test_email_message.py'
--- a/bzrlib/tests/test_email_message.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/test_email_message.py	2007-07-19 21:22:53 +0000
@@ -0,0 +1,226 @@
+# Copyright (C) 2007 Canonical Ltd
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+from email.Header import decode_header
+
+from bzrlib import __version__ as _bzrlib_version
+from bzrlib.email_message import EmailMessage
+from bzrlib.errors import BzrBadParameterNotUnicode
+from bzrlib.smtp_connection import SMTPConnection
+from bzrlib.tests import TestCase
+
+EMPTY_MESSAGE = '''\
+From: from at from.com
+Subject: subject
+To: to at to.com
+User-Agent: Bazaar (%s)
+
+''' % _bzrlib_version
+
+_SIMPLE_MESSAGE = '''\
+MIME-Version: 1.0
+Content-Type: text/plain; charset="%%s"
+Content-Transfer-Encoding: %%s
+From: from at from.com
+Subject: subject
+To: to at to.com
+User-Agent: Bazaar (%s)
+
+%%s''' % _bzrlib_version
+
+SIMPLE_MESSAGE_ASCII = _SIMPLE_MESSAGE % ('us-ascii', '7bit', 'body')
+SIMPLE_MESSAGE_UTF8 = _SIMPLE_MESSAGE % ('utf-8', 'base64', 'YsOzZHk=\n')
+SIMPLE_MESSAGE_8BIT = _SIMPLE_MESSAGE % ('8-bit', 'base64', 'YvNkeQ==\n')
+
+
+BOUNDARY = '=====123456=='
+
+_MULTIPART_HEAD = '''\
+Content-Type: multipart/mixed; boundary="%(boundary)s"
+MIME-Version: 1.0
+From: from at from.com
+Subject: subject
+To: to at to.com
+User-Agent: Bazaar (%(version)s)
+
+--%(boundary)s
+MIME-Version: 1.0
+Content-Type: text/plain; charset="us-ascii"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+body
+''' %  { 'version': _bzrlib_version, 'boundary': BOUNDARY }
+
+SIMPLE_MULTIPART_MESSAGE = _MULTIPART_HEAD + '--%s--' % BOUNDARY
+
+COMPLEX_MULTIPART_MESSAGE = _MULTIPART_HEAD + '''\
+--%(boundary)s
+MIME-Version: 1.0
+Content-Type: text/%%s; charset="us-ascii"; name="lines.txt"
+Content-Transfer-Encoding: 7bit
+Content-Disposition: inline
+
+a
+b
+c
+d
+e
+
+--%(boundary)s--''' %  { 'boundary': BOUNDARY }
+
+
+class TestEmailMessage(TestCase):
+
+    def test_empty_message(self):
+        msg = EmailMessage('from at from.com', 'to at to.com', 'subject')
+        self.assertEqualDiff(EMPTY_MESSAGE , msg.as_string())
+
+    def test_simple_message(self):
+        pairs = {
+            'body': SIMPLE_MESSAGE_ASCII,
+            u'b\xf3dy': SIMPLE_MESSAGE_UTF8,
+            'b\xc3\xb3dy': SIMPLE_MESSAGE_UTF8,
+            'b\xf3dy': SIMPLE_MESSAGE_8BIT,
+        }
+        for body, expected in pairs.items():
+            msg = EmailMessage('from at from.com', 'to at to.com', 'subject', body)
+            self.assertEqualDiff(expected, msg.as_string())
+
+    def test_multipart_message(self):
+        msg = EmailMessage('from at from.com', 'to at to.com', 'subject')
+        msg.add_inline_attachment('body')
+        self.assertEqualDiff(SIMPLE_MULTIPART_MESSAGE, msg.as_string(BOUNDARY))
+
+        msg = EmailMessage('from at from.com', 'to at to.com', 'subject', 'body')
+        msg.add_inline_attachment(u'a\nb\nc\nd\ne\n', 'lines.txt', 'x-subtype')
+        self.assertEqualDiff(COMPLEX_MULTIPART_MESSAGE % 'x-subtype',
+                msg.as_string(BOUNDARY))
+
+    def test_headers_accept_unicode_and_utf8(self):
+        for user in [ u'Pepe P\xe9rez <pperez at ejemplo.com>',
+                'Pepe P\xc3\xa9red <pperez at ejemplo.com>' ]:
+            msg = EmailMessage(user, user, user) # no exception raised
+
+            for header in ['From', 'To', 'Subject']:
+                value = msg[header]
+                str(value).decode('ascii') # no UnicodeDecodeError
+
+    def test_headers_reject_8bit(self):
+        for i in range(3): # from_address, to_address, subject
+            x = [ '"J. Random Developer" <jrandom at example.com>' ] * 3
+            x[i] = 'Pepe P\xe9rez <pperez at ejemplo.com>'
+            self.assertRaises(BzrBadParameterNotUnicode, EmailMessage, *x)
+
+    def test_multiple_destinations(self):
+        to_addresses = [ 'to1 at to.com', 'to2 at to.com', 'to3 at to.com' ]
+        msg = EmailMessage('from at from.com', to_addresses, 'subject')
+        self.assertContainsRe(msg.as_string(), 'To: ' +
+                ', '.join(to_addresses)) # re.M can't be passed, so no ^$
+
+    def test_retrieving_headers(self):
+        msg = EmailMessage('from at from.com', 'to at to.com', 'subject')
+        for header, value in [('From', 'from at from.com'), ('To', 'to at to.com'),
+                ('Subject', 'subject')]:
+            self.assertEqual(value, msg.get(header))
+            self.assertEqual(value, msg[header])
+        self.assertEqual(None, msg.get('Does-Not-Exist'))
+        self.assertEqual(None, msg['Does-Not-Exist'])
+        self.assertEqual('None', msg.get('Does-Not-Exist', 'None'))
+
+    def test_setting_headers(self):
+        msg = EmailMessage('from at from.com', 'to at to.com', 'subject')
+        msg['To'] = 'to2 at to.com'
+        msg['Cc'] = 'cc at cc.com'
+        self.assertEqual('to2 at to.com', msg['To'])
+        self.assertEqual('cc at cc.com', msg['Cc'])
+
+    def test_send(self):
+        class FakeConfig:
+            def get_user_option(self, option):
+                return None
+
+        messages = []
+
+        def send_as_append(_self, msg):
+            messages.append(msg.as_string(BOUNDARY))
+
+        old_send_email = SMTPConnection.send_email
+        try:
+            SMTPConnection.send_email = send_as_append
+
+            EmailMessage.send(FakeConfig(), 'from at from.com', 'to at to.com',
+                    'subject', 'body', u'a\nb\nc\nd\ne\n', 'lines.txt')
+            self.assertEqualDiff(COMPLEX_MULTIPART_MESSAGE % 'plain',
+                    messages[0])
+            messages[:] = []
+
+            EmailMessage.send(FakeConfig(), 'from at from.com', 'to at to.com',
+                    'subject', 'body', u'a\nb\nc\nd\ne\n', 'lines.txt',
+                    'x-patch')
+            self.assertEqualDiff(COMPLEX_MULTIPART_MESSAGE % 'x-patch',
+                    messages[0])
+            messages[:] = []
+
+            EmailMessage.send(FakeConfig(), 'from at from.com', 'to at to.com',
+                    'subject', 'body')
+            self.assertEqualDiff(SIMPLE_MESSAGE_ASCII , messages[0])
+            messages[:] = []
+        finally:
+            SMTPConnection.send_email = old_send_email
+
+    def test_address_to_encoded_header(self):
+        def decode(s):
+            """Convert a RFC2047-encoded string to a unicode string."""
+            return ' '.join([chunk.decode(encoding or 'ascii')
+                             for chunk, encoding in decode_header(s)])
+
+        address = 'jrandom at example.com'
+        encoded = EmailMessage.address_to_encoded_header(address)
+        self.assertEqual(address, encoded)
+
+        address = 'J Random Developer <jrandom at example.com>'
+        encoded = EmailMessage.address_to_encoded_header(address)
+        self.assertEqual(address, encoded)
+
+        address = '"J. Random Developer" <jrandom at example.com>'
+        encoded = EmailMessage.address_to_encoded_header(address)
+        self.assertEqual(address, encoded)
+
+        address = u'Pepe P\xe9rez <pperez at ejemplo.com>' # unicode ok
+        encoded = EmailMessage.address_to_encoded_header(address)
+        self.assert_('pperez at ejemplo.com' in encoded) # addr must be unencoded
+        self.assertEquals(address, decode(encoded))
+
+        address = 'Pepe P\xc3\xa9red <pperez at ejemplo.com>' # UTF-8 ok
+        encoded = EmailMessage.address_to_encoded_header(address)
+        self.assert_('pperez at ejemplo.com' in encoded)
+        self.assertEquals(address, decode(encoded).encode('utf-8'))
+
+        address = 'Pepe P\xe9rez <pperez at ejemplo.com>' # ISO-8859-1 not ok
+        self.assertRaises(BzrBadParameterNotUnicode,
+                EmailMessage.address_to_encoded_header, address)
+
+    def test_string_with_encoding(self):
+        pairs = {
+                u'Pepe':        ('Pepe', 'ascii'),
+                u'P\xe9rez':    ('P\xc3\xa9rez', 'utf-8'),
+                'Perez':         ('Perez', 'ascii'), # u'Pepe' == 'Pepe'
+                'P\xc3\xa9rez': ('P\xc3\xa9rez', 'utf-8'),
+                'P\xe9rez':     ('P\xe9rez', '8-bit'),
+        }
+        for string_, pair in pairs.items():
+            self.assertEqual(pair, EmailMessage.string_with_encoding(string_))

=== modified file 'NEWS'
--- a/NEWS	2007-07-20 12:56:33 +0000
+++ b/NEWS	2007-07-20 14:28:59 +0000
@@ -82,6 +82,8 @@
     * RevisionTree.get_weave is now deprecated.  Tree.plan_merge is now used
       for performing annotate-merge.  (Aaron Bentley)
 
+    * New EmailMessage class to create email messages. (Adeodato Sim??)
+
   TESTING:
 
     * Remove selftest ``--clean-output``, ``--numbered-dirs`` and

=== modified file 'bzrlib/merge_directive.py'
--- a/bzrlib/merge_directive.py	2007-07-19 18:16:11 +0000
+++ b/bzrlib/merge_directive.py	2007-07-20 14:28:59 +0000
@@ -15,7 +15,6 @@
 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
 
-from email import Message
 from StringIO import StringIO
 import re
 
@@ -33,6 +32,7 @@
 from bzrlib.bundle import (
     serializer as bundle_serializer,
     )
+from bzrlib.email_message import EmailMessage
 
 
 class _BaseMergeDirective(object):
@@ -170,19 +170,16 @@
         :return: an email message
         """
         mail_from = branch.get_config().username()
-        message = Message.Message()
-        message['To'] = mail_to
-        message['From'] = mail_from
         if self.message is not None:
-            message['Subject'] = self.message
+            subject = self.message
         else:
             revision = branch.repository.get_revision(self.revision_id)
-            message['Subject'] = revision.message
+            subject = revision.message
         if sign:
             body = self.to_signed(branch)
         else:
             body = ''.join(self.to_lines())
-        message.set_payload(body)
+        message = EmailMessage(mail_from, mail_to, subject, body)
         return message
 
     def install_revisions(self, target_repo):

=== modified file 'bzrlib/smtp_connection.py'
--- a/bzrlib/smtp_connection.py	2007-06-25 13:17:32 +0000
+++ b/bzrlib/smtp_connection.py	2007-07-18 15:51:52 +0000
@@ -79,15 +79,18 @@
     def get_message_addresses(message):
         """Get the origin and destination addresses of a message.
 
-        :param message: An email.Message or email.MIMEMultipart object.
+        :param message: A message object supporting get() to access its
+            headers, like email.Message or bzrlib.email_message.EmailMessage.
         :return: A pair (from_email, to_emails), where from_email is the email
             address in the From header, and to_emails a list of all the
             addresses in the To, Cc, and Bcc headers.
         """
-        from_email = Utils.parseaddr(message['From'])[1]
+        from_email = Utils.parseaddr(message.get('From', None))[1]
         to_full_addresses = []
         for header in ['To', 'Cc', 'Bcc']:
-            to_full_addresses += message.get_all(header, [])
+            value = message.get(header, None)
+            if value:
+                to_full_addresses.append(value)
         to_emails = [ pair[1] for pair in
                 Utils.getaddresses(to_full_addresses) ]
 

=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py	2007-07-20 00:58:41 +0000
+++ b/bzrlib/tests/__init__.py	2007-07-20 14:28:59 +0000
@@ -2268,6 +2268,7 @@
                    'bzrlib.tests.test_deprecated_graph',
                    'bzrlib.tests.test_diff',
                    'bzrlib.tests.test_dirstate',
+                   'bzrlib.tests.test_email_message',
                    'bzrlib.tests.test_errors',
                    'bzrlib.tests.test_escaped_store',
                    'bzrlib.tests.test_extract',

=== modified file 'bzrlib/tests/blackbox/test_merge_directive.py'
--- a/bzrlib/tests/blackbox/test_merge_directive.py	2007-07-19 18:16:11 +0000
+++ b/bzrlib/tests/blackbox/test_merge_directive.py	2007-07-20 14:28:59 +0000
@@ -25,9 +25,10 @@
     )
 
 
-EMAIL1 = """To: pqm at example.com
-From: J. Random Hacker <jrandom at example.com>
+EMAIL1 = """From: "J. Random Hacker" <jrandom at example.com>
 Subject: bar
+To: pqm at example.com
+User-Agent: Bazaar \(.*\)
 
 # Bazaar merge directive format 2 \\(Bazaar 0.19\\)
 # revision_id: bar-id

=== modified file 'bzrlib/tests/test_merge_directive.py'
--- a/bzrlib/tests/test_merge_directive.py	2007-07-19 18:16:11 +0000
+++ b/bzrlib/tests/test_merge_directive.py	2007-07-20 14:28:59 +0000
@@ -251,9 +251,10 @@
         md.bundle = value
 
 
-EMAIL1 = """To: pqm at example.com
-From: J. Random Hacker <jrandom at example.com>
+EMAIL1 = """From: "J. Random Hacker" <jrandom at example.com>
 Subject: Commit of rev2a
+To: pqm at example.com
+User-Agent: Bazaar \(.*\)
 
 # Bazaar merge directive format 1
 # revision_id: rev2a
@@ -264,9 +265,10 @@
 """
 
 
-EMAIL1_2 = """To: pqm at example.com
-From: J. Random Hacker <jrandom at example.com>
+EMAIL1_2 = """From: "J. Random Hacker" <jrandom at example.com>
 Subject: Commit of rev2a
+To: pqm at example.com
+User-Agent: Bazaar \(.*\)
 
 # Bazaar merge directive format 2 \\(Bazaar 0.19\\)
 # revision_id: rev2a
@@ -277,9 +279,10 @@
 """
 
 
-EMAIL2 = """To: pqm at example.com
-From: J. Random Hacker <jrandom at example.com>
+EMAIL2 = """From: "J. Random Hacker" <jrandom at example.com>
 Subject: Commit of rev2a with special message
+To: pqm at example.com
+User-Agent: Bazaar \(.*\)
 
 # Bazaar merge directive format 1
 # revision_id: rev2a
@@ -290,9 +293,10 @@
 # message: Commit of rev2a with special message
 """
 
-EMAIL2_2 = """To: pqm at example.com
-From: J. Random Hacker <jrandom at example.com>
+EMAIL2_2 = """From: "J. Random Hacker" <jrandom at example.com>
 Subject: Commit of rev2a with special message
+To: pqm at example.com
+User-Agent: Bazaar \(.*\)
 
 # Bazaar merge directive format 2 \\(Bazaar 0.19\\)
 # revision_id: rev2a

=== modified file 'bzrlib/tests/test_smtp_connection.py'
--- a/bzrlib/tests/test_smtp_connection.py	2007-06-23 01:11:51 +0000
+++ b/bzrlib/tests/test_smtp_connection.py	2007-07-18 15:51:52 +0000
@@ -18,6 +18,7 @@
 from email.Message import Message
 
 from bzrlib import config
+from bzrlib.email_message import EmailMessage
 from bzrlib.errors import NoDestinationAddress
 from bzrlib.tests import TestCase
 from bzrlib.smtp_connection import SMTPConnection
@@ -72,6 +73,17 @@
         self.assertEqual(sorted(['john at doe.com', 'jane at doe.com',
             'pperez at ejemplo.com', 'user at localhost']), sorted(to))
 
+        # now with bzrlib's EmailMessage
+        msg = EmailMessage('"J. Random Developer" <jrandom at example.com>', [
+            'John Doe <john at doe.com>', 'Jane Doe <jane at doe.com>',
+            u'Pepe P\xe9rez <pperez at ejemplo.com>', 'user at localhost' ],
+            'subject')
+
+        from_, to = SMTPConnection.get_message_addresses(msg)
+        self.assertEqual('jrandom at example.com', from_)
+        self.assertEqual(sorted(['john at doe.com', 'jane at doe.com',
+            'pperez at ejemplo.com', 'user at localhost']), sorted(to))
+
     def test_destination_address_required(self):
         class FakeConfig:
             def get_user_option(self, option):
@@ -81,3 +93,11 @@
         msg['From'] = '"J. Random Developer" <jrandom at example.com>'
         self.assertRaises(NoDestinationAddress,
                 SMTPConnection(FakeConfig()).send_email, msg)
+
+        msg = EmailMessage('from at from.com', '', 'subject')
+        self.assertRaises(NoDestinationAddress,
+                SMTPConnection(FakeConfig()).send_email, msg)
+
+        msg = EmailMessage('from at from.com', [], 'subject')
+        self.assertRaises(NoDestinationAddress,
+                SMTPConnection(FakeConfig()).send_email, msg)



More information about the bazaar-commits mailing list