Rev 31: Allow setting the Message-Id field to enable threading in mail clients. in http://bzr.arbash-meinel.com/plugins/email
John Arbash Meinel
john at arbash-meinel.com
Tue Mar 18 17:10:57 GMT 2008
At http://bzr.arbash-meinel.com/plugins/email
------------------------------------------------------------
revno: 31
revision-id: john at arbash-meinel.com-20080318170912-u01q3u6ue6pt4u1w
parent: robertc at robertcollins.net-20071114180445-o0zqawr901smlcn2
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: email
timestamp: Tue 2008-03-18 12:09:12 -0500
message:
Allow setting the Message-Id field to enable threading in mail clients.
modified:
emailer.py emailer.py-20070123220937-ec5y2n2oeoa0p4ue-1
smtp_connection.py smtp_connection.py-20070125220755-k6ueimjqwn16wvr9-1
tests/test_smtp_connection.py test_smtp_connection-20070125220755-k6ueimjqwn16wvr9-2
tests/testemail.py testpublish.py-20051018071212-e3a53d78c05e0e0a
-------------- next part --------------
=== modified file 'emailer.py'
--- a/emailer.py 2007-09-19 21:38:05 +0000
+++ b/emailer.py 2008-03-18 17:09:12 +0000
@@ -206,6 +206,19 @@
else:
raise
+ def message_id(self):
+ return "<%s>" % (self.revision.revision_id,)
+
+ def in_reply_to(self):
+ if self.revision.parent_ids:
+ return "<%s>" % (self.revision.parent_ids[0],)
+ else:
+ return None
+
+ def references(self):
+ # At the moment, we only pay attention to the first parent
+ return self.in_reply_to()
+
def _send_using_smtplib(self):
"""Use python's smtplib to send the email."""
body = self.body()
@@ -215,14 +228,29 @@
to_addrs = self.to()
if isinstance(to_addrs, basestring):
to_addrs = [to_addrs]
+ extra_headers = None
+ if self.should_include_message_id():
+ extra_headers = {'Message-Id': self.message_id(),
+ 'In-Reply-To': self.in_reply_to(),
+ 'References': self.references(),
+ }
smtp = self._smtplib_implementation(self.config)
smtp.send_text_and_attachment_email(from_addr, to_addrs,
subject, body, diff,
- self.diff_filename())
+ self.diff_filename(),
+ extra_headers=extra_headers)
+
+ def should_include_message_id(self):
+ val = self.config.get_user_option('bzr_email_include_message_id')
+ if val is None:
+ return False
+ if val.lower() in ('t', 'y', 'true', 'yes', '1', 'on'):
+ return True
+ return False
def should_send(self):
- return self.to() and self.from_address()
+ return bool(self.to() and self.from_address())
def send_maybe(self):
if self.should_send():
@@ -237,3 +265,41 @@
def diff_filename(self):
return "patch-%s.diff" % (self.revno,)
+
+# This doesn't include @ or _ since those have extra special meaning
+# We use '_' as our escape char, and @ needs to be present at the end.
+# '.' is allowed because all of our sections are 'dot-atom' compatible
+_allowed_chars = set('abcdefghijklmnopqrstuvwxyz'
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+ '0123456789'
+ '!#$%&\'*+-/=?^`{|}~'
+ '.'
+ )
+
+_charmap = None
+
+def _get_charmap():
+ global _charmap
+ if _charmap is None:
+ _charmap = []
+ for idx in xrange(128):
+ c = chr(idx)
+ if c in _allowed_chars:
+ _charmap.append(c)
+ else:
+ _charmap.append('_%02x' % (idx,))
+ for idx in xrange(128, 256):
+ _charmap.append('_%02x' % (idx,))
+ return _charmap
+
+
+def message_id_from_revision_id(revision_id):
+ """Compute a valid message id from a given revision_id.
+
+ This is an attempt to conform to RFC2822 Section 3.6.4
+ http://www.faqs.org/rfcs/rfc2822
+ """
+ char_map = _get_charmap()
+
+ rev_portion = ''.join(char_map[ord(c)] for c in revision_id)
+ return '<' + rev_portion + '@bzr-email>'
=== modified file 'smtp_connection.py'
--- a/smtp_connection.py 2007-01-26 15:23:32 +0000
+++ b/smtp_connection.py 2008-03-18 17:09:12 +0000
@@ -197,7 +197,8 @@
def send_text_and_attachment_email(self, from_address, to_addresses,
subject, message, attachment_text,
- attachment_filename='patch.diff'):
+ attachment_filename='patch.diff',
+ extra_headers=None):
"""Send a Unicode message and an 8-bit attachment.
See create_email for common parameter definitions.
@@ -209,6 +210,9 @@
"""
msg, from_email, to_emails = self.create_email(from_address,
to_addresses, subject, message)
+ if extra_headers:
+ for hdr, value in extra_headers.iteritems():
+ msg[hdr] = Header(value)
# Must be an 8-bit string
assert isinstance(attachment_text, str)
=== modified file 'tests/test_smtp_connection.py'
--- a/tests/test_smtp_connection.py 2007-01-26 17:03:29 +0000
+++ b/tests/test_smtp_connection.py 2008-03-18 17:09:12 +0000
@@ -184,6 +184,51 @@
'--=====123456==--'
) % _bzrlib_version, conn.actions[1][3])
+ def test_send_text_and_attachment_email_extra_headers(self):
+ conn = self.get_connection('')
+ from_addr = u'Joe Foo <joe at foo.com>'
+ to_addr = u'Jerry Foo <jerry at foo.com>'
+ subject = u'My Commit'
+ message = u'Hello'
+ diff_txt = ('=== diff contents\n'
+ '--- old\n'
+ '+++ new\n'
+ ' unchanged\n'
+ )
+ extra_headers = {'Foo': 'bar'}
+ conn.send_text_and_attachment_email(from_addr, [to_addr], subject,
+ message, diff_txt, 'test.diff',
+ extra_headers=extra_headers)
+ self.assertEqual(('create_connection',), conn.actions[0])
+ self.assertEqual(('sendmail', 'joe at foo.com', ['jerry at foo.com']),
+ conn.actions[1][:3])
+ self.assertEqualDiff((
+ 'Content-Type: multipart/mixed; boundary="=====123456=="\n'
+ 'MIME-Version: 1.0\n'
+ 'From: Joe Foo <joe at foo.com>\n'
+ 'User-Agent: bzr/%s\n'
+ 'To: Jerry Foo <jerry at foo.com>\n'
+ 'Subject: My Commit\n'
+ 'Foo: bar\n'
+ '\n'
+ '--=====123456==\n'
+ 'Content-Type: text/plain; charset="utf-8"\n'
+ 'MIME-Version: 1.0\n'
+ 'Content-Transfer-Encoding: base64\n'
+ '\n'
+ 'SGVsbG8=\n'
+ '\n'
+ '--=====123456==\n'
+ 'Content-Type: text/plain; charset="8-bit"; name="test.diff"\n'
+ 'MIME-Version: 1.0\n'
+ 'Content-Transfer-Encoding: base64\n'
+ 'Content-Disposition: inline; filename="test.diff"\n'
+ '\n'
+ 'PT09IGRpZmYgY29udGVudHMKLS0tIG9sZAorKysgbmV3CiB1bmNoYW5nZWQK\n'
+ '\n'
+ '--=====123456==--'
+ ) % _bzrlib_version, conn.actions[1][3])
+
def test_create_and_send(self):
"""Test that you can create a custom email, and send it."""
conn = self.get_connection('')
=== modified file 'tests/testemail.py'
--- a/tests/testemail.py 2007-02-17 18:09:12 +0000
+++ b/tests/testemail.py 2008-03-18 17:09:12 +0000
@@ -24,7 +24,7 @@
)
from bzrlib.bzrdir import BzrDir
from bzrlib.tests import TestCaseInTempDir
-from bzrlib.plugins.email import post_commit
+from bzrlib.plugins.email import emailer, post_commit
from bzrlib.plugins.email.emailer import EmailSender
@@ -54,6 +54,9 @@
"post_commit_to=demo at example.com\n"
"post_commit_sender=Sample <foo at example.com>\n")
+include_message_id = ("[DEFAULT]\n"
+ "bzr_email_include_message_id = True\n")
+
class TestGetTo(TestCaseInTempDir):
@@ -92,6 +95,25 @@
sender = self.get_sender(unconfigured_config)
self.assertEqual('Robert <foo at example.com>', sender.from_address())
+ def test_message_id_and_in_reply_to_no_parents(self):
+ sender = self.get_sender()
+ self.assertEqual('<A>', sender.message_id())
+ self.assertIs(None, sender.in_reply_to())
+
+ def test_message_and_in_reply_to_one_parent(self):
+ sender = self.get_sender_with_two_commits()
+ self.assertEqual('<rev-second>', sender.message_id())
+ self.assertEqual('<rev-first>', sender.in_reply_to())
+
+ def test_default_should_include_message_id(self):
+ sender = self.get_sender()
+ # Default is False
+ self.assertFalse(sender.should_include_message_id())
+
+ def test_should_include_message_id(self):
+ sender = self.get_sender(include_message_id)
+ self.assertTrue(sender.should_include_message_id())
+
def test_should_send(self):
sender = self.get_sender()
self.assertEqual(True, sender.should_send())
@@ -165,6 +187,31 @@
sender._setup_revision_and_revno()
return sender
+ def get_sender_with_two_commits(self):
+ self.branch = BzrDir.create_branch_convenience('.')
+ tree = self.branch.bzrdir.open_workingtree()
+ tree.commit('foo bar baz\nfuzzy\rwuzzy', rev_id='rev-first',
+ allow_pointless=True,
+ timestamp=1,
+ timezone=0,
+ committer="Sample <john at example.com>",
+ )
+ tree.commit('bear baby\n', rev_id='rev-second',
+ allow_pointless=True,
+ timestamp=1,
+ timezone=0,
+ committer="Sample <john at example.com>",
+ )
+ my_config = self.branch.get_config()
+ config_file = StringIO(sample_config)
+ (my_config._get_global_config()._get_parser(config_file))
+ sender = EmailSender(self.branch, 'rev-second', my_config)
+ # This is usually only done after the EmailSender has locked the branch
+ # and repository during send(), however, for testing, we need to do it
+ # earlier, since send() is not called.
+ sender._setup_revision_and_revno()
+ return sender
+
class TestEmailerWithLocal(tests.TestCaseWithTransport):
"""Test that Emailer will use a local branch if supplied."""
@@ -210,3 +257,20 @@
# We should be using the master repository here, because the child
# repository doesn't contain the revision.
self.assertIs(master_tree.branch.repository, sender.repository)
+
+
+
+class TestMessageIdFromRevisionId(tests.TestCase):
+
+ def assertMessageIdFromRevisionId(self, message_id, revision_id):
+ self.assertEqual(message_id,
+ emailer.message_id_from_revision_id(revision_id))
+
+ def test_simple_message_id(self):
+ self.assertMessageIdFromRevisionId('<A at bzr-email>', 'A')
+ self.assertMessageIdFromRevisionId('<A_40foobar.com at bzr-email>',
+ 'A at foobar.com')
+
+ def test_non_ascii(self):
+ self.assertMessageIdFromRevisionId('<x_c2_b5_40b_5f_c3_a5 at bzr-email>',
+ 'x\xc2\xb5 at b_\xc3\xa5')
More information about the bazaar-commits
mailing list