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