/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
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
4183.7.1 by Sabin Iacob
update FSF mailing address
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
16
6379.6.7 by Jelmer Vernooij
Move importing from future until after doc string, otherwise the doc string will disappear.
17
"""A convenience class around smtplib."""
18
6379.6.3 by Jelmer Vernooij
Use absolute_import.
19
from __future__ import absolute_import
20
6791.2.3 by Jelmer Vernooij
Fix more imports.
21
try:
6797 by Jelmer Vernooij
Merge lp:~jelmer/brz/fix-imports.
22
    from email.utils import getaddresses, parseaddr
6791.2.3 by Jelmer Vernooij
Fix more imports.
23
except ImportError:  # python < 3
6797 by Jelmer Vernooij
Merge lp:~jelmer/brz/fix-imports.
24
    from email.Utils import getaddresses, parseaddr
6791.2.3 by Jelmer Vernooij
Fix more imports.
25
2694.2.1 by Aaron Bentley
Make error handling nicer when SMTP server not working
26
import errno
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
27
import smtplib
2694.2.1 by Aaron Bentley
Make error handling nicer when SMTP server not working
28
import socket
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
29
6624 by Jelmer Vernooij
Merge Python3 porting work ('py3 pokes')
30
from . import (
2900.2.11 by Vincent Ladeuil
Make smtp aware of authentication config.
31
    config,
4147.1.3 by James Henstridge
Switch to using osutils.safe_utf8() as suggested by Vincent. Also
32
    osutils,
2900.2.11 by Vincent Ladeuil
Make smtp aware of authentication config.
33
    )
6624 by Jelmer Vernooij
Merge Python3 porting work ('py3 pokes')
34
from .errors import (
6734.1.1 by Jelmer Vernooij
Fix more imports.
35
    BzrError,
36
    InternalBzrError,
2694.2.1 by Aaron Bentley
Make error handling nicer when SMTP server not working
37
    )
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
38
39
6379.8.1 by Jelmer Vernooij
Register smtp options in bzrlib.config, and use config stacks in bzrlib.smtp_conection.
40
smtp_password = config.Option('smtp_password', default=None,
41
        help='''\
42
Password to use for authentication to SMTP server.
43
''')
44
smtp_server = config.Option('smtp_server', default=None,
45
        help='''\
46
Hostname of the SMTP server to use for sending email.
47
''')
48
smtp_username = config.Option('smtp_username', default=None,
49
        help='''\
50
Username to use for authentication to SMTP server.
51
''')
52
53
6734.1.1 by Jelmer Vernooij
Fix more imports.
54
class SMTPError(BzrError):
55
56
    _fmt = "SMTP error: %(error)s"
57
58
    def __init__(self, error):
59
        self.error = error
60
61
62
class SMTPConnectionRefused(SMTPError):
63
64
    _fmt = "SMTP connection to %(host)s refused"
65
66
    def __init__(self, error, host):
67
        self.error = error
68
        self.host = host
69
70
71
class DefaultSMTPConnectionRefused(SMTPConnectionRefused):
72
73
    _fmt = "Please specify smtp_server.  No server at default %(host)s."
74
75
76
77
class NoDestinationAddress(InternalBzrError):
78
79
    _fmt = "Message does not have a destination address."
80
81
82
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
83
class SMTPConnection(object):
84
    """Connect to an SMTP server and send an email.
85
6622.1.34 by Jelmer Vernooij
Rename brzlib => breezy.
86
    This is a gateway between breezy.config.Config and smtplib.SMTP. It
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
87
    understands the basic bzr SMTP configuration information: smtp_server,
88
    smtp_username, and smtp_password.
89
    """
90
91
    _default_smtp_server = 'localhost'
92
2694.2.1 by Aaron Bentley
Make error handling nicer when SMTP server not working
93
    def __init__(self, config, _smtp_factory=None):
94
        self._smtp_factory = _smtp_factory
95
        if self._smtp_factory is None:
96
            self._smtp_factory = smtplib.SMTP
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
97
        self._config = config
6379.8.1 by Jelmer Vernooij
Register smtp options in bzrlib.config, and use config stacks in bzrlib.smtp_conection.
98
        self._config_smtp_server = config.get('smtp_server')
2694.2.1 by Aaron Bentley
Make error handling nicer when SMTP server not working
99
        self._smtp_server = self._config_smtp_server
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
100
        if self._smtp_server is None:
101
            self._smtp_server = self._default_smtp_server
102
6379.8.1 by Jelmer Vernooij
Register smtp options in bzrlib.config, and use config stacks in bzrlib.smtp_conection.
103
        self._smtp_username = config.get('smtp_username')
104
        self._smtp_password = config.get('smtp_password')
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
105
106
        self._connection = None
107
108
    def _connect(self):
109
        """If we haven't connected, connect and authenticate."""
110
        if self._connection is not None:
111
            return
112
113
        self._create_connection()
4222.3.10 by Jelmer Vernooij
Avoid using the default username in the case of SMTP.
114
        # FIXME: _authenticate() should only be called when the server has
115
        # refused unauthenticated access, so it can safely try to authenticate 
116
        # with the default username. JRV20090407
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
117
        self._authenticate()
118
119
    def _create_connection(self):
120
        """Create an SMTP connection."""
2694.2.1 by Aaron Bentley
Make error handling nicer when SMTP server not working
121
        self._connection = self._smtp_factory()
122
        try:
123
            self._connection.connect(self._smtp_server)
6619.3.2 by Jelmer Vernooij
Apply 2to3 except fix.
124
        except socket.error as e:
2694.2.1 by Aaron Bentley
Make error handling nicer when SMTP server not working
125
            if e.args[0] == errno.ECONNREFUSED:
126
                if self._config_smtp_server is None:
127
                    raise DefaultSMTPConnectionRefused(socket.error,
128
                                                       self._smtp_server)
129
                else:
130
                    raise SMTPConnectionRefused(socket.error,
131
                                                self._smtp_server)
132
            else:
133
                raise
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
134
2898.2.3 by James Henstridge
* Fix merge-directive blackbox test.
135
        # Say EHLO (falling back to HELO) to query the server's features.
2898.2.1 by James Henstridge
Update SMTPConnection._create_connection to better follow the SMTP
136
        code, resp = self._connection.ehlo()
137
        if not (200 <= code <= 299):
138
            code, resp = self._connection.helo()
139
            if not (200 <= code <= 299):
140
                raise SMTPError("server refused HELO: %d %s" % (code, resp))
141
142
        # Use TLS if the server advertised it:
143
        if self._connection.has_extn("starttls"):
144
            code, resp = self._connection.starttls()
145
            if not (200 <= code <= 299):
146
                raise SMTPError("server refused STARTTLS: %d %s" % (code, resp))
147
            # Say EHLO again, to check for newly revealed features
148
            code, resp = self._connection.ehlo()
149
            if not (200 <= code <= 299):
150
                raise SMTPError("server refused EHLO: %d %s" % (code, resp))
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
151
152
    def _authenticate(self):
153
        """If necessary authenticate yourself to the server."""
2900.2.15 by Vincent Ladeuil
AuthenticationConfig can be queried for logins too (first step).
154
        auth = config.AuthenticationConfig()
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
155
        if self._smtp_username is None:
4222.3.10 by Jelmer Vernooij
Avoid using the default username in the case of SMTP.
156
            # FIXME: Since _authenticate gets called even when no authentication
157
            # is necessary, it's not possible to use the default username 
158
            # here yet.
4304.2.1 by Vincent Ladeuil
Fix bug #367726 by reverting some default user handling introduced
159
            self._smtp_username = auth.get_user('smtp', self._smtp_server)
160
            if self._smtp_username is None:
2900.2.15 by Vincent Ladeuil
AuthenticationConfig can be queried for logins too (first step).
161
                return
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
162
2900.2.12 by Vincent Ladeuil
Since all schemes query AuthenticationConfig then prompt user, make that
163
        if self._smtp_password is None:
164
            self._smtp_password = auth.get_password(
165
                'smtp', self._smtp_server, self._smtp_username)
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
166
4147.1.1 by James Henstridge
Ensure that byte strings are passed to SMTP.login(), as passing unicode
167
        # smtplib requires that the username and password be byte
4147.1.2 by James Henstridge
Encode usernames and passwords as UTF-8 rather than ASCII. While
168
        # strings.  The CRAM-MD5 spec doesn't give any guidance on
4147.1.3 by James Henstridge
Switch to using osutils.safe_utf8() as suggested by Vincent. Also
169
        # encodings, but the SASL PLAIN spec says UTF-8, so that's
170
        # what we'll use.
171
        username = osutils.safe_utf8(self._smtp_username)
172
        password = osutils.safe_utf8(self._smtp_password)
4147.1.1 by James Henstridge
Ensure that byte strings are passed to SMTP.login(), as passing unicode
173
4147.1.3 by James Henstridge
Switch to using osutils.safe_utf8() as suggested by Vincent. Also
174
        self._connection.login(username, password)
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
175
176
    @staticmethod
177
    def get_message_addresses(message):
178
        """Get the origin and destination addresses of a message.
179
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
180
        :param message: A message object supporting get() to access its
6791.2.3 by Jelmer Vernooij
Fix more imports.
181
            headers, like email.message.Message or
182
            breezy.email_message.EmailMessage.
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
183
        :return: A pair (from_email, to_emails), where from_email is the email
184
            address in the From header, and to_emails a list of all the
185
            addresses in the To, Cc, and Bcc headers.
186
        """
6797 by Jelmer Vernooij
Merge lp:~jelmer/brz/fix-imports.
187
        from_email = parseaddr(message.get('From', None))[1]
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
188
        to_full_addresses = []
189
        for header in ['To', 'Cc', 'Bcc']:
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
190
            value = message.get(header, None)
191
            if value:
192
                to_full_addresses.append(value)
2535.2.3 by Adeodato Simó
Import full email.Utils module instead of individual functions, as per
193
        to_emails = [ pair[1] for pair in
6797 by Jelmer Vernooij
Merge lp:~jelmer/brz/fix-imports.
194
                getaddresses(to_full_addresses) ]
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
195
196
        return from_email, to_emails
197
198
    def send_email(self, message):
199
        """Send an email message.
200
2547.1.1 by Aaron Bentley
Add SMTPConnection class (Adeodato Simó)
201
        The message will be sent to all addresses in the To, Cc and Bcc
202
        headers.
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
203
6791.2.3 by Jelmer Vernooij
Fix more imports.
204
        :param message: An email.message.Message or
205
            email.mime.multipart.MIMEMultipart object.
2535.2.1 by Adeodato Simó
New SMTPConnection class, a reduced version of that in bzr-email.
206
        :return: None
207
        """
208
        from_email, to_emails = self.get_message_addresses(message)
209
210
        if not to_emails:
211
            raise NoDestinationAddress
212
213
        try:
214
            self._connect()
2547.1.1 by Aaron Bentley
Add SMTPConnection class (Adeodato Simó)
215
            self._connection.sendmail(from_email, to_emails,
216
                                      message.as_string())
6619.3.2 by Jelmer Vernooij
Apply 2to3 except fix.
217
        except smtplib.SMTPRecipientsRefused as e:
2535.2.4 by Adeodato Simó
Don't use BzrCommandError in non-UI code; create and use an SMTPError
218
            raise SMTPError('server refused recipient: %d %s' %
6656.1.1 by Martin
Apply 2to3 dict fixer and clean up resulting mess using view helpers
219
                    next(iter(e.recipients.values())))
6619.3.2 by Jelmer Vernooij
Apply 2to3 except fix.
220
        except smtplib.SMTPResponseException as e:
2535.2.4 by Adeodato Simó
Don't use BzrCommandError in non-UI code; create and use an SMTPError
221
            raise SMTPError('%d %s' % (e.smtp_code, e.smtp_error))
6619.3.2 by Jelmer Vernooij
Apply 2to3 except fix.
222
        except smtplib.SMTPException as e:
2535.2.4 by Adeodato Simó
Don't use BzrCommandError in non-UI code; create and use an SMTPError
223
            raise SMTPError(str(e))