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