/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 breezy/smtp_connection.py

  • Committer: Robert Collins
  • Date: 2007-04-19 02:27:44 UTC
  • mto: This revision was merged to the branch mainline in revision 2426.
  • Revision ID: robertc@robertcollins.net-20070419022744-pfdqz42kp1wizh43
``make docs`` now creates a man page at ``man1/bzr.1`` fixing bug 107388.
(Robert Collins)

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 email.utils import getaddresses, parseaddr
20
 
 
21
 
import errno
22
 
import smtplib
23
 
import socket
24
 
 
25
 
from . import (
26
 
    config,
27
 
    osutils,
28
 
    )
29
 
from .errors import (
30
 
    BzrError,
31
 
    InternalBzrError,
32
 
    )
33
 
 
34
 
 
35
 
smtp_password = config.Option('smtp_password', default=None,
36
 
                              help='''\
37
 
Password to use for authentication to SMTP server.
38
 
''')
39
 
smtp_server = config.Option('smtp_server', default=None,
40
 
                            help='''\
41
 
Hostname of the SMTP server to use for sending email.
42
 
''')
43
 
smtp_username = config.Option('smtp_username', default=None,
44
 
                              help='''\
45
 
Username to use for authentication to SMTP server.
46
 
''')
47
 
 
48
 
 
49
 
class SMTPError(BzrError):
50
 
 
51
 
    _fmt = "SMTP error: %(error)s"
52
 
 
53
 
    def __init__(self, error):
54
 
        self.error = error
55
 
 
56
 
 
57
 
class SMTPConnectionRefused(SMTPError):
58
 
 
59
 
    _fmt = "SMTP connection to %(host)s refused"
60
 
 
61
 
    def __init__(self, error, host):
62
 
        self.error = error
63
 
        self.host = host
64
 
 
65
 
 
66
 
class DefaultSMTPConnectionRefused(SMTPConnectionRefused):
67
 
 
68
 
    _fmt = "Please specify smtp_server.  No server at default %(host)s."
69
 
 
70
 
 
71
 
class NoDestinationAddress(InternalBzrError):
72
 
 
73
 
    _fmt = "Message does not have a destination address."
74
 
 
75
 
 
76
 
class SMTPConnection(object):
77
 
    """Connect to an SMTP server and send an email.
78
 
 
79
 
    This is a gateway between breezy.config.Config and smtplib.SMTP. It
80
 
    understands the basic bzr SMTP configuration information: smtp_server,
81
 
    smtp_username, and smtp_password.
82
 
    """
83
 
 
84
 
    _default_smtp_server = 'localhost'
85
 
 
86
 
    def __init__(self, config, _smtp_factory=None):
87
 
        self._smtp_factory = _smtp_factory
88
 
        if self._smtp_factory is None:
89
 
            self._smtp_factory = smtplib.SMTP
90
 
        self._config = config
91
 
        self._config_smtp_server = config.get('smtp_server')
92
 
        self._smtp_server = self._config_smtp_server
93
 
        if self._smtp_server is None:
94
 
            self._smtp_server = self._default_smtp_server
95
 
 
96
 
        self._smtp_username = config.get('smtp_username')
97
 
        self._smtp_password = config.get('smtp_password')
98
 
 
99
 
        self._connection = None
100
 
 
101
 
    def _connect(self):
102
 
        """If we haven't connected, connect and authenticate."""
103
 
        if self._connection is not None:
104
 
            return
105
 
 
106
 
        self._create_connection()
107
 
        # FIXME: _authenticate() should only be called when the server has
108
 
        # refused unauthenticated access, so it can safely try to authenticate
109
 
        # with the default username. JRV20090407
110
 
        self._authenticate()
111
 
 
112
 
    def _create_connection(self):
113
 
        """Create an SMTP connection."""
114
 
        self._connection = self._smtp_factory()
115
 
        try:
116
 
            self._connection.connect(self._smtp_server)
117
 
        except socket.error as e:
118
 
            if e.args[0] == errno.ECONNREFUSED:
119
 
                if self._config_smtp_server is None:
120
 
                    raise DefaultSMTPConnectionRefused(socket.error,
121
 
                                                       self._smtp_server)
122
 
                else:
123
 
                    raise SMTPConnectionRefused(socket.error,
124
 
                                                self._smtp_server)
125
 
            else:
126
 
                raise
127
 
 
128
 
        # Say EHLO (falling back to HELO) to query the server's features.
129
 
        code, resp = self._connection.ehlo()
130
 
        if not (200 <= code <= 299):
131
 
            code, resp = self._connection.helo()
132
 
            if not (200 <= code <= 299):
133
 
                raise SMTPError("server refused HELO: %d %s" % (code, resp))
134
 
 
135
 
        # Use TLS if the server advertised it:
136
 
        if self._connection.has_extn("starttls"):
137
 
            code, resp = self._connection.starttls()
138
 
            if not (200 <= code <= 299):
139
 
                raise SMTPError("server refused STARTTLS: %d %s" %
140
 
                                (code, resp))
141
 
            # Say EHLO again, to check for newly revealed features
142
 
            code, resp = self._connection.ehlo()
143
 
            if not (200 <= code <= 299):
144
 
                raise SMTPError("server refused EHLO: %d %s" % (code, resp))
145
 
 
146
 
    def _authenticate(self):
147
 
        """If necessary authenticate yourself to the server."""
148
 
        auth = config.AuthenticationConfig()
149
 
        if self._smtp_username is None:
150
 
            # FIXME: Since _authenticate gets called even when no authentication
151
 
            # is necessary, it's not possible to use the default username
152
 
            # here yet.
153
 
            self._smtp_username = auth.get_user('smtp', self._smtp_server)
154
 
            if self._smtp_username is None:
155
 
                return
156
 
 
157
 
        if self._smtp_password is None:
158
 
            self._smtp_password = auth.get_password(
159
 
                'smtp', self._smtp_server, self._smtp_username)
160
 
 
161
 
        # smtplib requires that the username and password be byte
162
 
        # strings.  The CRAM-MD5 spec doesn't give any guidance on
163
 
        # encodings, but the SASL PLAIN spec says UTF-8, so that's
164
 
        # what we'll use.
165
 
        username = osutils.safe_utf8(self._smtp_username)
166
 
        password = osutils.safe_utf8(self._smtp_password)
167
 
 
168
 
        self._connection.login(username, password)
169
 
 
170
 
    @staticmethod
171
 
    def get_message_addresses(message):
172
 
        """Get the origin and destination addresses of a message.
173
 
 
174
 
        :param message: A message object supporting get() to access its
175
 
            headers, like email.message.Message or
176
 
            breezy.email_message.EmailMessage.
177
 
        :return: A pair (from_email, to_emails), where from_email is the email
178
 
            address in the From header, and to_emails a list of all the
179
 
            addresses in the To, Cc, and Bcc headers.
180
 
        """
181
 
        from_email = parseaddr(message.get('From', None))[1]
182
 
        to_full_addresses = []
183
 
        for header in ['To', 'Cc', 'Bcc']:
184
 
            value = message.get(header, None)
185
 
            if value:
186
 
                to_full_addresses.append(value)
187
 
        to_emails = [pair[1] for pair in
188
 
                     getaddresses(to_full_addresses)]
189
 
 
190
 
        return from_email, to_emails
191
 
 
192
 
    def send_email(self, message):
193
 
        """Send an email message.
194
 
 
195
 
        The message will be sent to all addresses in the To, Cc and Bcc
196
 
        headers.
197
 
 
198
 
        :param message: An email.message.Message or
199
 
            email.mime.multipart.MIMEMultipart object.
200
 
        :return: None
201
 
        """
202
 
        from_email, to_emails = self.get_message_addresses(message)
203
 
 
204
 
        if not to_emails:
205
 
            raise NoDestinationAddress
206
 
 
207
 
        try:
208
 
            self._connect()
209
 
            self._connection.sendmail(from_email, to_emails,
210
 
                                      message.as_string())
211
 
        except smtplib.SMTPRecipientsRefused as e:
212
 
            raise SMTPError('server refused recipient: %d %s' %
213
 
                            next(iter(e.recipients.values())))
214
 
        except smtplib.SMTPResponseException as e:
215
 
            raise SMTPError('%d %s' % (e.smtp_code, e.smtp_error))
216
 
        except smtplib.SMTPException as e:
217
 
            raise SMTPError(str(e))