/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
17
"""A convenience class around smtplib and email."""
18
19
from email.Header import Header
20
from email.Message import Message
21
try:
22
    # python <= 2.4
23
    from email.MIMEText import MIMEText
24
    from email.MIMEMultipart import MIMEMultipart
0.175.8 by John Arbash Meinel
Figure out and test how to read back one of these Unicode aware emails.
25
    from email.Utils import parseaddr
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
26
except ImportError:
27
    # python 2.5 moved MIMEText into a better namespace
28
    from email.mime.text import MIMEText
29
    from email.mime.multipart import MIMEMultipart
0.175.8 by John Arbash Meinel
Figure out and test how to read back one of these Unicode aware emails.
30
    from email.utils import parseaddr
0.171.31 by Scott Wilson
Raise the right errors when smtp connection, etc fails. (bug #224202)
31
import socket
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
32
import smtplib
33
34
from bzrlib import (
0.171.31 by Scott Wilson
Raise the right errors when smtp connection, etc fails. (bug #224202)
35
    errors,
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
36
    ui,
37
    __version__ as _bzrlib_version,
38
    )
39
40
41
class SMTPConnection(object):
42
    """Connect to an SMTP server and send an email.
43
44
    This is a gateway between bzrlib.config.Config and smtplib.SMTP. It
45
    understands the basic bzr SMTP configuration information.
46
    """
47
48
    _default_smtp_server = 'localhost'
49
50
    def __init__(self, config):
51
        self._config = config
52
        self._smtp_server = config.get_user_option('smtp_server')
53
        if self._smtp_server is None:
54
            self._smtp_server = self._default_smtp_server
55
56
        self._smtp_username = config.get_user_option('smtp_username')
57
        self._smtp_password = config.get_user_option('smtp_password')
58
59
        self._connection = None
60
61
    def _connect(self):
62
        """If we haven't connected, connect and authenticate."""
63
        if self._connection is not None:
64
            return
65
66
        self._create_connection()
67
        self._authenticate()
68
69
    def _create_connection(self):
70
        """Create an SMTP connection."""
71
        self._connection = smtplib.SMTP()
0.171.31 by Scott Wilson
Raise the right errors when smtp connection, etc fails. (bug #224202)
72
        try:
73
            self._connection.connect(self._smtp_server)
74
        except socket.error, e:
75
            raise errors.SocketConnectionError(
76
                host=self._smtp_server,
77
                msg="Unable to connect to smtp server to send email to",
78
                orig_error=e)
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
79
80
        # If this fails, it just returns an error, but it shouldn't raise an
81
        # exception unless something goes really wrong (in which case we want
82
        # to fail anyway).
0.171.40 by Martin Pool
Ignore failures to STARTTLS
83
        try:
84
            self._connection.starttls()
85
        except smtplib.SMTPException, e:
86
            if e.args[0] == 'STARTTLS extension not supported by server.':
87
                # python2.6 changed to raising an exception here; we can't
88
                # really do anything else without it so just continue
89
                # <https://bugs.edge.launchpad.net/bzr-email/+bug/335332>
90
                pass
91
            else:
92
                raise
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
93
94
    def _authenticate(self):
95
        """If necessary authenticate yourself to the server."""
96
        if self._smtp_username is None:
97
            return
98
99
        if self._smtp_password is None:
100
            self._smtp_password = ui.ui_factory.get_password(
0.175.10 by John Arbash Meinel
Use the correct password request string
101
                'Please enter the SMTP password: %(user)s@%(host)s',
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
102
                user=self._smtp_username,
103
                host=self._smtp_server)
104
        try:
105
            self._connection.login(self._smtp_username, self._smtp_password)
106
        except smtplib.SMTPHeloError, e:
0.171.31 by Scott Wilson
Raise the right errors when smtp connection, etc fails. (bug #224202)
107
            raise errors.BzrCommandError('SMTP server refused HELO: %d %s'
108
                                         % (e.smtp_code, e.smtp_error))
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
109
        except smtplib.SMTPAuthenticationError, e:
0.171.31 by Scott Wilson
Raise the right errors when smtp connection, etc fails. (bug #224202)
110
            raise errors.BzrCommandError('SMTP server refused authentication: %d %s'
111
                                         % (e.smtp_code, e.smtp_error))
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
112
        except smtplib.SMTPException, e:
0.171.31 by Scott Wilson
Raise the right errors when smtp connection, etc fails. (bug #224202)
113
            raise errors.BzrCommandError(str(e))
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
114
115
    @staticmethod
116
    def _split_address(address):
117
        """Split an username + email address into its parts.
118
119
        This takes "Joe Foo <joe@foo.com>" and returns "Joe Foo",
120
        "joe@foo.com".
121
        :param address: A combined username
122
        :return: (username, email)
123
        """
0.175.8 by John Arbash Meinel
Figure out and test how to read back one of these Unicode aware emails.
124
        return parseaddr(address)
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
125
126
    def _basic_message(self, from_address, to_addresses, subject):
127
        """Create the basic Message using the right Header info.
128
129
        This creates an email Message with no payload.
130
        :param from_address: The Unicode from address.
131
        :param to_addresses: A list of Unicode destination addresses.
132
        :param subject: A Unicode subject for the email.
133
        """
134
        # It would be nice to use a single part if we only had one, but we
135
        # would have to know ahead of time how many parts we needed.
136
        # So instead, just default to multipart.
137
        msg = MIMEMultipart()
138
139
        # Header() does a good job of doing the proper encoding. However it
140
        # confuses my SMTP server because it doesn't decode the strings. So it
141
        # is better to send the addresses as:
142
        #   =?utf-8?q?username?= <email@addr.com>
143
        # Which is how Thunderbird does it
144
145
        from_user, from_email = self._split_address(from_address)
0.175.11 by John Arbash Meinel
Cleanup from review comments by Marius Gedminas
146
        msg['From'] = '%s <%s>' % (Header(unicode(from_user)), from_email)
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
147
        msg['User-Agent'] = 'bzr/%s' % _bzrlib_version
148
149
        to_emails = []
150
        to_header = []
151
        for addr in to_addresses:
152
            to_user, to_email = self._split_address(addr)
153
            to_emails.append(to_email)
0.175.11 by John Arbash Meinel
Cleanup from review comments by Marius Gedminas
154
            to_header.append('%s <%s>' % (Header(unicode(to_user)), to_email))
0.175.7 by John Arbash Meinel
split out SMTPConnection to its own file.
155
156
        msg['To'] = ', '.join(to_header)
157
        msg['Subject'] = Header(subject)
158
        return msg, from_email, to_emails
159
160
    def create_email(self, from_address, to_addresses, subject, text):
161
        """Create an email.Message object.
162
163
        This function allows you to create a basic email, and then add extra
164
        payload to it.
165
166
        :param from_address: A Unicode string with the source email address.
167
            Example: u'Joe B\xe5 <joe@bar.com>'
168
        :param to_addresses: A list of addresses to send to.
169
            Example: [u'Joe B\xe5 <joe@bar.com>', u'Lilly <lilly@nowhere.com>']
170
        :param subject: A Unicode Subject for the email.
171
            Example: u'Use Bazaar, its c\xb5l'
172
        :param text: A Unicode message (will be encoded into utf-8)
173
            Example: u'I started using Bazaar today.\nI highly recommend it.\n'
174
        :return: (email_message, from_email, to_emails)
175
            email_message: is a MIME wrapper with the email headers setup. You
176
                can add more payload by using .attach()
177
            from_email: the email address extracted from from_address
178
            to_emails: the list of email addresses extracted from to_addresses
179
        """
180
        msg, from_email, to_emails = self._basic_message(from_address,
181
                                                         to_addresses, subject)
182
        payload = MIMEText(text.encode('utf-8'), 'plain', 'utf-8')
183
        msg.attach(payload)
184
        return msg, from_email, to_emails
185
186
    def send_email(self, email_message, from_email, to_emails):
187
        """Actually send an email to the server.
188
189
        If your requirements are simple, you can simply:
190
        smtp.send_email(*smtp.create_email(...))
191
        because the parameters passed to send_email() are the same as the
192
        parameters returned from create_email.
193
194
        :param email_message: An email.Message object. You can just pass the
195
            value from create_email().
196
        :param from_email: The email address to send from. Usually just the
197
            value returned from create_email()
198
        :param to_emails: A list of emails to send to.
199
        :return: None
200
        """
201
        self._connect()
202
        self._connection.sendmail(from_email, to_emails,
203
                                  email_message.as_string())
204
205
    def send_text_email(self, from_address, to_addresses, subject, message):
206
        """Send a single text-only email.
207
208
        This is a helper when you know you are just sending a simple text
209
        message. See create_email for an explanation of parameters.
210
        """
211
        msg, from_email, to_emails = self.create_email(from_address,
212
                                            to_addresses, subject, message)
213
        self.send_email(msg, from_email, to_emails)
214
215
    def send_text_and_attachment_email(self, from_address, to_addresses,
216
                                       subject, message, attachment_text,
217
                                       attachment_filename='patch.diff'):
218
        """Send a Unicode message and an 8-bit attachment.
219
220
        See create_email for common parameter definitions.
221
        :param attachment_text: This is assumed to be an 8-bit text attachment.
222
            This assumes you want the attachment to be shown in the email.
223
            So don't use this for binary file attachments.
224
        :param attachment_filename: The name for the attachement. This will
225
            give a default name for email programs to save the attachment.
226
        """
227
        msg, from_email, to_emails = self.create_email(from_address,
228
                                            to_addresses, subject, message)
229
        # Must be an 8-bit string
230
        assert isinstance(attachment_text, str)
231
232
        diff_payload = MIMEText(attachment_text, 'plain', '8-bit')
233
        # Override Content-Type so that we can include the name
234
        content_type = diff_payload['Content-Type']
235
        content_type += '; name="%s"' % (attachment_filename,)
236
        diff_payload.replace_header('Content-Type', content_type)
237
        diff_payload['Content-Disposition'] = ('inline; filename="%s"'
238
                                               % (attachment_filename,))
239
        msg.attach(diff_payload)
240
        self.send_email(msg, from_email, to_emails)