/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/smtp_connection.py

  • Committer: Vincent Ladeuil
  • Date: 2012-01-18 14:09:19 UTC
  • mto: This revision was merged to the branch mainline in revision 6468.
  • Revision ID: v.ladeuil+lp@free.fr-20120118140919-rlvdrhpc0nq1lbwi
Change set/remove to require a lock for the branch config files.

This means that tests (or any plugin for that matter) do not requires an
explicit lock on the branch anymore to change a single option. This also
means the optimisation becomes "opt-in" and as such won't be as
spectacular as it may be and/or harder to get right (nothing fails
anymore).

This reduces the diff by ~300 lines.

Code/tests that were updating more than one config option is still taking
a lock to at least avoid some IOs and demonstrate the benefits through
the decreased number of hpss calls.

The duplication between BranchStack and BranchOnlyStack will be removed
once the same sharing is in place for local config files, at which point
the Stack class itself may be able to host the changes.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2007 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""A convenience class around smtplib."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
from email import Utils
 
22
import errno
 
23
import smtplib
 
24
import socket
 
25
 
 
26
from bzrlib import (
 
27
    config,
 
28
    osutils,
 
29
    )
 
30
from bzrlib.errors import (
 
31
    NoDestinationAddress,
 
32
    SMTPError,
 
33
    DefaultSMTPConnectionRefused,
 
34
    SMTPConnectionRefused,
 
35
    )
 
36
 
 
37
 
 
38
smtp_password = config.Option('smtp_password', default=None,
 
39
        help='''\
 
40
Password to use for authentication to SMTP server.
 
41
''')
 
42
smtp_server = config.Option('smtp_server', default=None,
 
43
        help='''\
 
44
Hostname of the SMTP server to use for sending email.
 
45
''')
 
46
smtp_username = config.Option('smtp_username', default=None,
 
47
        help='''\
 
48
Username to use for authentication to SMTP server.
 
49
''')
 
50
 
 
51
 
 
52
class SMTPConnection(object):
 
53
    """Connect to an SMTP server and send an email.
 
54
 
 
55
    This is a gateway between bzrlib.config.Config and smtplib.SMTP. It
 
56
    understands the basic bzr SMTP configuration information: smtp_server,
 
57
    smtp_username, and smtp_password.
 
58
    """
 
59
 
 
60
    _default_smtp_server = 'localhost'
 
61
 
 
62
    def __init__(self, config, _smtp_factory=None):
 
63
        self._smtp_factory = _smtp_factory
 
64
        if self._smtp_factory is None:
 
65
            self._smtp_factory = smtplib.SMTP
 
66
        self._config = config
 
67
        self._config_smtp_server = config.get('smtp_server')
 
68
        self._smtp_server = self._config_smtp_server
 
69
        if self._smtp_server is None:
 
70
            self._smtp_server = self._default_smtp_server
 
71
 
 
72
        self._smtp_username = config.get('smtp_username')
 
73
        self._smtp_password = config.get('smtp_password')
 
74
 
 
75
        self._connection = None
 
76
 
 
77
    def _connect(self):
 
78
        """If we haven't connected, connect and authenticate."""
 
79
        if self._connection is not None:
 
80
            return
 
81
 
 
82
        self._create_connection()
 
83
        # FIXME: _authenticate() should only be called when the server has
 
84
        # refused unauthenticated access, so it can safely try to authenticate 
 
85
        # with the default username. JRV20090407
 
86
        self._authenticate()
 
87
 
 
88
    def _create_connection(self):
 
89
        """Create an SMTP connection."""
 
90
        self._connection = self._smtp_factory()
 
91
        try:
 
92
            self._connection.connect(self._smtp_server)
 
93
        except socket.error, e:
 
94
            if e.args[0] == errno.ECONNREFUSED:
 
95
                if self._config_smtp_server is None:
 
96
                    raise DefaultSMTPConnectionRefused(socket.error,
 
97
                                                       self._smtp_server)
 
98
                else:
 
99
                    raise SMTPConnectionRefused(socket.error,
 
100
                                                self._smtp_server)
 
101
            else:
 
102
                raise
 
103
 
 
104
        # Say EHLO (falling back to HELO) to query the server's features.
 
105
        code, resp = self._connection.ehlo()
 
106
        if not (200 <= code <= 299):
 
107
            code, resp = self._connection.helo()
 
108
            if not (200 <= code <= 299):
 
109
                raise SMTPError("server refused HELO: %d %s" % (code, resp))
 
110
 
 
111
        # Use TLS if the server advertised it:
 
112
        if self._connection.has_extn("starttls"):
 
113
            code, resp = self._connection.starttls()
 
114
            if not (200 <= code <= 299):
 
115
                raise SMTPError("server refused STARTTLS: %d %s" % (code, resp))
 
116
            # Say EHLO again, to check for newly revealed features
 
117
            code, resp = self._connection.ehlo()
 
118
            if not (200 <= code <= 299):
 
119
                raise SMTPError("server refused EHLO: %d %s" % (code, resp))
 
120
 
 
121
    def _authenticate(self):
 
122
        """If necessary authenticate yourself to the server."""
 
123
        auth = config.AuthenticationConfig()
 
124
        if self._smtp_username is None:
 
125
            # FIXME: Since _authenticate gets called even when no authentication
 
126
            # is necessary, it's not possible to use the default username 
 
127
            # here yet.
 
128
            self._smtp_username = auth.get_user('smtp', self._smtp_server)
 
129
            if self._smtp_username is None:
 
130
                return
 
131
 
 
132
        if self._smtp_password is None:
 
133
            self._smtp_password = auth.get_password(
 
134
                'smtp', self._smtp_server, self._smtp_username)
 
135
 
 
136
        # smtplib requires that the username and password be byte
 
137
        # strings.  The CRAM-MD5 spec doesn't give any guidance on
 
138
        # encodings, but the SASL PLAIN spec says UTF-8, so that's
 
139
        # what we'll use.
 
140
        username = osutils.safe_utf8(self._smtp_username)
 
141
        password = osutils.safe_utf8(self._smtp_password)
 
142
 
 
143
        self._connection.login(username, password)
 
144
 
 
145
    @staticmethod
 
146
    def get_message_addresses(message):
 
147
        """Get the origin and destination addresses of a message.
 
148
 
 
149
        :param message: A message object supporting get() to access its
 
150
            headers, like email.Message or bzrlib.email_message.EmailMessage.
 
151
        :return: A pair (from_email, to_emails), where from_email is the email
 
152
            address in the From header, and to_emails a list of all the
 
153
            addresses in the To, Cc, and Bcc headers.
 
154
        """
 
155
        from_email = Utils.parseaddr(message.get('From', None))[1]
 
156
        to_full_addresses = []
 
157
        for header in ['To', 'Cc', 'Bcc']:
 
158
            value = message.get(header, None)
 
159
            if value:
 
160
                to_full_addresses.append(value)
 
161
        to_emails = [ pair[1] for pair in
 
162
                Utils.getaddresses(to_full_addresses) ]
 
163
 
 
164
        return from_email, to_emails
 
165
 
 
166
    def send_email(self, message):
 
167
        """Send an email message.
 
168
 
 
169
        The message will be sent to all addresses in the To, Cc and Bcc
 
170
        headers.
 
171
 
 
172
        :param message: An email.Message or email.MIMEMultipart object.
 
173
        :return: None
 
174
        """
 
175
        from_email, to_emails = self.get_message_addresses(message)
 
176
 
 
177
        if not to_emails:
 
178
            raise NoDestinationAddress
 
179
 
 
180
        try:
 
181
            self._connect()
 
182
            self._connection.sendmail(from_email, to_emails,
 
183
                                      message.as_string())
 
184
        except smtplib.SMTPRecipientsRefused, e:
 
185
            raise SMTPError('server refused recipient: %d %s' %
 
186
                    e.recipients.values()[0])
 
187
        except smtplib.SMTPResponseException, e:
 
188
            raise SMTPError('%d %s' % (e.smtp_code, e.smtp_error))
 
189
        except smtplib.SMTPException, e:
 
190
            raise SMTPError(str(e))