Rev 2548: Add SMTPConnection class (Adeodato Simó) in file:///home/pqm/archives/thelove/bzr/%2Btrunk/

Canonical.com Patch Queue Manager pqm at pqm.ubuntu.com
Mon Jun 25 14:46:13 BST 2007


At file:///home/pqm/archives/thelove/bzr/%2Btrunk/

------------------------------------------------------------
revno: 2548
revision-id: pqm at pqm.ubuntu.com-20070625134610-4y70duw4fcuj8txe
parent: pqm at pqm.ubuntu.com-20070625092303-yr8bqbke8snrmkig
parent: abentley at panoramicfeedback.com-20070625131732-8jp7229jrt5r7khp
committer: Canonical.com Patch Queue Manager<pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Mon 2007-06-25 14:46:10 +0100
message:
  Add SMTPConnection class (Adeodato Simó)
added:
  bzrlib/smtp_connection.py      smtp_connection.py-20070618204456-nu6wag1ste4biuk2-1
  bzrlib/tests/test_smtp_connection.py test_smtp_connection-20070618204509-wuyxc0r0ztrecv7e-1
modified:
  NEWS                           NEWS-20050323055033-4e00b5db738777ff
  bzrlib/builtins.py             builtins.py-20050830033751-fc01482b9ca23183
  bzrlib/errors.py               errors.py-20050309040759-20512168c4e14fbd
  bzrlib/tests/__init__.py       selftest.py-20050531073622-8d0e3c8845c97a64
  bzrlib/tests/blackbox/test_merge_directive.py test_merge_directive-20070302012039-zh7uhy39biairtn0-1
  doc/configuration.txt          configuration.txt-20060314161707-868350809502af01
    ------------------------------------------------------------
    revno: 2547.1.1
    merged: abentley at panoramicfeedback.com-20070625131732-8jp7229jrt5r7khp
    parent: pqm at pqm.ubuntu.com-20070625092303-yr8bqbke8snrmkig
    parent: dato at net.com.org.es-20070623011151-i21l3zr5igkphd53
    committer: Aaron Bentley <abentley at panoramicfeedback.com>
    branch nick: Aaron's integration
    timestamp: Mon 2007-06-25 09:17:32 -0400
    message:
      Add SMTPConnection class (Adeodato Simó)
    ------------------------------------------------------------
    revno: 2535.2.5
    merged: dato at net.com.org.es-20070623011151-i21l3zr5igkphd53
    parent: dato at net.com.org.es-20070620003933-cjj1lq1lqgav5gm6
    committer: Adeodato Simó <dato at net.com.org.es>
    branch nick: bzr.smtp_connection
    timestamp: Sat 2007-06-23 02:11:51 +0100
    message:
      Fix copyright statement not to contain "by".
    ------------------------------------------------------------
    revno: 2535.2.4
    merged: dato at net.com.org.es-20070620003933-cjj1lq1lqgav5gm6
    parent: dato at net.com.org.es-20070620002245-t4ugu7418qmkdtmv
    committer: Adeodato Simó <dato at net.com.org.es>
    branch nick: bzr.smtp_connection
    timestamp: Wed 2007-06-20 01:39:33 +0100
    message:
      Don't use BzrCommandError in non-UI code; create and use an SMTPError
      exception instead.
    ------------------------------------------------------------
    revno: 2535.2.3
    merged: dato at net.com.org.es-20070620002245-t4ugu7418qmkdtmv
    parent: dato at net.com.org.es-20070619201517-0v2w6kvc9ur7ybpg
    committer: Adeodato Simó <dato at net.com.org.es>
    branch nick: bzr.smtp_connection
    timestamp: Wed 2007-06-20 01:22:45 +0100
    message:
      Import full email.Utils module instead of individual functions, as per
      Aaron's review.
    ------------------------------------------------------------
    revno: 2535.2.2
    merged: dato at net.com.org.es-20070619201517-0v2w6kvc9ur7ybpg
    parent: dato at net.com.org.es-20070619161126-zwni0l40maepwyj5
    committer: Adeodato Simó <dato at net.com.org.es>
    branch nick: bzr.smtp_connection
    timestamp: Tue 2007-06-19 21:15:17 +0100
    message:
      Swap the order of internal_error and _fmt for consistency.
    ------------------------------------------------------------
    revno: 2535.2.1
    merged: dato at net.com.org.es-20070619161126-zwni0l40maepwyj5
    parent: pqm at pqm.ubuntu.com-20070619024533-oand7e7ns9eyis9x
    committer: Adeodato Simó <dato at net.com.org.es>
    branch nick: bzr.smtp_connection
    timestamp: Tue 2007-06-19 17:11:26 +0100
    message:
      New SMTPConnection class, a reduced version of that in bzr-email.
=== added file 'bzrlib/smtp_connection.py'
--- a/bzrlib/smtp_connection.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/smtp_connection.py	2007-06-25 13:17:32 +0000
@@ -0,0 +1,120 @@
+# 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 smtplib."""
+
+from email import Utils
+import smtplib
+
+from bzrlib import ui
+from bzrlib.errors import NoDestinationAddress, SMTPError
+
+
+class SMTPConnection(object):
+    """Connect to an SMTP server and send an email.
+
+    This is a gateway between bzrlib.config.Config and smtplib.SMTP. It
+    understands the basic bzr SMTP configuration information: smtp_server,
+    smtp_username, and smtp_password.
+    """
+
+    _default_smtp_server = 'localhost'
+
+    def __init__(self, config):
+        self._config = config
+        self._smtp_server = config.get_user_option('smtp_server')
+        if self._smtp_server is None:
+            self._smtp_server = self._default_smtp_server
+
+        self._smtp_username = config.get_user_option('smtp_username')
+        self._smtp_password = config.get_user_option('smtp_password')
+
+        self._connection = None
+
+    def _connect(self):
+        """If we haven't connected, connect and authenticate."""
+        if self._connection is not None:
+            return
+
+        self._create_connection()
+        self._authenticate()
+
+    def _create_connection(self):
+        """Create an SMTP connection."""
+        self._connection = smtplib.SMTP()
+        self._connection.connect(self._smtp_server)
+
+        # If this fails, it just returns an error, but it shouldn't raise an
+        # exception unless something goes really wrong (in which case we want
+        # to fail anyway).
+        self._connection.starttls()
+
+    def _authenticate(self):
+        """If necessary authenticate yourself to the server."""
+        if self._smtp_username is None:
+            return
+
+        if self._smtp_password is None:
+            self._smtp_password = ui.ui_factory.get_password(
+                'Please enter the SMTP password: %(user)s@%(host)s',
+                user=self._smtp_username,
+                host=self._smtp_server)
+
+        self._connection.login(self._smtp_username, self._smtp_password)
+
+    @staticmethod
+    def get_message_addresses(message):
+        """Get the origin and destination addresses of a message.
+
+        :param message: An email.Message or email.MIMEMultipart object.
+        :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]
+        to_full_addresses = []
+        for header in ['To', 'Cc', 'Bcc']:
+            to_full_addresses += message.get_all(header, [])
+        to_emails = [ pair[1] for pair in
+                Utils.getaddresses(to_full_addresses) ]
+
+        return from_email, to_emails
+
+    def send_email(self, message):
+        """Send an email message.
+
+        The message will be sent to all addresses in the To, Cc and Bcc
+        headers.
+
+        :param message: An email.Message or email.MIMEMultipart object.
+        :return: None
+        """
+        from_email, to_emails = self.get_message_addresses(message)
+
+        if not to_emails:
+            raise NoDestinationAddress
+
+        try:
+            self._connect()
+            self._connection.sendmail(from_email, to_emails,
+                                      message.as_string())
+        except smtplib.SMTPRecipientsRefused, e:
+            raise SMTPError('server refused recipient: %d %s' %
+                    e.recipients.values()[0])
+        except smtplib.SMTPResponseException, e:
+            raise SMTPError('%d %s' % (e.smtp_code, e.smtp_error))
+        except smtplib.SMTPException, e:
+            raise SMTPError(str(e))

=== added file 'bzrlib/tests/test_smtp_connection.py'
--- a/bzrlib/tests/test_smtp_connection.py	1970-01-01 00:00:00 +0000
+++ b/bzrlib/tests/test_smtp_connection.py	2007-06-23 01:11:51 +0000
@@ -0,0 +1,83 @@
+# Copyright (C) 2005, 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 cStringIO import StringIO
+from email.Message import Message
+
+from bzrlib import config
+from bzrlib.errors import NoDestinationAddress
+from bzrlib.tests import TestCase
+from bzrlib.smtp_connection import SMTPConnection
+
+
+class TestSMTPConnection(TestCase):
+
+    def get_connection(self, text):
+        my_config = config.GlobalConfig()
+        config_file = StringIO(text)
+        my_config._get_parser(config_file)
+        return SMTPConnection(my_config)
+
+    def test_defaults(self):
+        conn = self.get_connection('')
+        self.assertEqual('localhost', conn._smtp_server)
+        self.assertEqual(None, conn._smtp_username)
+        self.assertEqual(None, conn._smtp_password)
+
+    def test_smtp_server(self):
+        conn = self.get_connection('[DEFAULT]\nsmtp_server=host:10\n')
+        self.assertEqual('host:10', conn._smtp_server)
+
+    def test_smtp_username(self):
+        conn = self.get_connection('')
+        self.assertIs(None, conn._smtp_username)
+
+        conn = self.get_connection('[DEFAULT]\nsmtp_username=joebody\n')
+        self.assertEqual(u'joebody', conn._smtp_username)
+
+    def test_smtp_password(self):
+        conn = self.get_connection('')
+        self.assertIs(None, conn._smtp_password)
+
+        conn = self.get_connection('[DEFAULT]\nsmtp_password=mypass\n')
+        self.assertEqual(u'mypass', conn._smtp_password)
+
+    def test_get_message_addresses(self):
+        msg = Message()
+
+        from_, to = SMTPConnection.get_message_addresses(msg)
+        self.assertEqual('', from_)
+        self.assertEqual([], to)
+
+        msg['From'] = '"J. Random Developer" <jrandom at example.com>'
+        msg['To'] = 'John Doe <john at doe.com>, Jane Doe <jane at doe.com>'
+        msg['CC'] = u'Pepe P\xe9rez <pperez at ejemplo.com>'
+        msg['Bcc'] = 'user at localhost'
+
+        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):
+                return None
+
+        msg = Message()
+        msg['From'] = '"J. Random Developer" <jrandom at example.com>'
+        self.assertRaises(NoDestinationAddress,
+                SMTPConnection(FakeConfig()).send_email, msg)

=== modified file 'NEWS'
--- a/NEWS	2007-06-22 15:27:19 +0000
+++ b/NEWS	2007-06-25 13:17:32 +0000
@@ -41,6 +41,10 @@
     * The lsprof filename note is emitted via trace.note(), not standard
       output.  (Aaron Bentley)
 
+  INTERNALS:
+
+    * New SMTPConnection class to unify email handling.  (Adeodato Simó)
+
   TESTING:
 
     * Removed the ``--keep-output`` option from selftest and clean up test

=== modified file 'bzrlib/builtins.py'
--- a/bzrlib/builtins.py	2007-06-19 14:49:06 +0000
+++ b/bzrlib/builtins.py	2007-06-25 13:17:32 +0000
@@ -23,7 +23,6 @@
 lazy_import(globals(), """
 import codecs
 import errno
-import smtplib
 import sys
 import tempfile
 import time
@@ -56,6 +55,7 @@
 from bzrlib.bundle.apply_bundle import install_bundle, merge_bundle
 from bzrlib.conflicts import ConflictList
 from bzrlib.revisionspec import RevisionSpec
+from bzrlib.smtp_connection import SMTPConnection
 from bzrlib.workingtree import WorkingTree
 """)
 
@@ -3584,12 +3584,8 @@
                 self.outf.writelines(directive.to_lines())
         else:
             message = directive.to_email(mail_to, branch, sign)
-            s = smtplib.SMTP()
-            server = branch.get_config().get_user_option('smtp_server')
-            if not server:
-                server = 'localhost'
-            s.connect(server)
-            s.sendmail(message['From'], message['To'], message.as_string())
+            s = SMTPConnection(branch.get_config())
+            s.send_email(message)
 
 
 class cmd_tag(Command):

=== modified file 'bzrlib/errors.py'
--- a/bzrlib/errors.py	2007-06-20 18:45:23 +0000
+++ b/bzrlib/errors.py	2007-06-25 13:17:32 +0000
@@ -2136,3 +2136,18 @@
 
     def __init__(self, response_tuple):
         self.response_tuple = response_tuple
+
+
+class NoDestinationAddress(BzrError):
+
+    _fmt = "Message does not have a destination address."
+
+    internal_error = True
+
+
+class SMTPError(BzrError):
+
+    _fmt = "SMTP error: %(error)s"
+
+    def __init__(self, error):
+        self.error = error

=== modified file 'bzrlib/tests/__init__.py'
--- a/bzrlib/tests/__init__.py	2007-06-18 20:04:16 +0000
+++ b/bzrlib/tests/__init__.py	2007-06-19 16:11:26 +0000
@@ -2299,6 +2299,7 @@
                    'bzrlib.tests.test_smart',
                    'bzrlib.tests.test_smart_add',
                    'bzrlib.tests.test_smart_transport',
+                   'bzrlib.tests.test_smtp_connection',
                    'bzrlib.tests.test_source',
                    'bzrlib.tests.test_ssh_transport',
                    'bzrlib.tests.test_status',

=== modified file 'bzrlib/tests/blackbox/test_merge_directive.py'
--- a/bzrlib/tests/blackbox/test_merge_directive.py	2007-06-19 14:49:06 +0000
+++ b/bzrlib/tests/blackbox/test_merge_directive.py	2007-06-25 13:17:32 +0000
@@ -109,15 +109,20 @@
         connect_calls = []
         def connect(self, host='localhost', port=0):
             connect_calls.append((self, host, port))
+        def starttls(self):
+            pass
         old_sendmail = smtplib.SMTP.sendmail
         smtplib.SMTP.sendmail = sendmail
         old_connect = smtplib.SMTP.connect
         smtplib.SMTP.connect = connect
+        old_starttls = smtplib.SMTP.starttls
+        smtplib.SMTP.starttls = starttls
         try:
             result = self.run_bzr(*args, **kwargs)
         finally:
             smtplib.SMTP.sendmail = old_sendmail
             smtplib.SMTP.connect = old_connect
+            smtplib.SMTP.starttls = old_starttls
         return result + (connect_calls, sendmail_calls)
 
     def test_mail_default(self):
@@ -132,8 +137,8 @@
         self.assertEqual(('localhost', 0), call[1:3])
         self.assertEqual(1, len(sendmail_calls))
         call = sendmail_calls[0]
-        self.assertEqual(('J. Random Hacker <jrandom at example.com>',
-                          'pqm at example.com'), call[1:3])
+        self.assertEqual(('jrandom at example.com', ['pqm at example.com']),
+                call[1:3])
         self.assertContainsRe(call[3], EMAIL1)
 
     def test_pull_raw(self):

=== modified file 'doc/configuration.txt'
--- a/doc/configuration.txt	2007-04-23 01:30:35 +0000
+++ b/doc/configuration.txt	2007-06-19 16:11:26 +0000
@@ -180,6 +180,16 @@
 
     gpg_signing_command = /usr/bin/gnpg
 
+smtp_server
+-----------
+(Default: "localhost"). SMTP server to use when Bazaar needs to send
+email, eg. with ``merge-directive --mail-to``, or the bzr-email plugin.
+
+smtp_username, smtp_password
+----------------------------
+User and password to authenticate to the SMTP server. If smtp_username
+is set, and smtp_password is not, Bazaar will prompt for a password.
+
 
 Branch 6 Options
 ================




More information about the bazaar-commits mailing list