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