summaryrefslogtreecommitdiff
path: root/bzrlib/smtp_connection.py
blob: d36a01da9e37960f8462b431a250ac15b1722bf2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""A convenience class around smtplib."""

from __future__ import absolute_import

from email import Utils
import errno
import smtplib
import socket

from bzrlib import (
    config,
    osutils,
    )
from bzrlib.errors import (
    NoDestinationAddress,
    SMTPError,
    DefaultSMTPConnectionRefused,
    SMTPConnectionRefused,
    )


smtp_password = config.Option('smtp_password', default=None,
        help='''\
Password to use for authentication to SMTP server.
''')
smtp_server = config.Option('smtp_server', default=None,
        help='''\
Hostname of the SMTP server to use for sending email.
''')
smtp_username = config.Option('smtp_username', default=None,
        help='''\
Username to use for authentication to SMTP server.
''')


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, _smtp_factory=None):
        self._smtp_factory = _smtp_factory
        if self._smtp_factory is None:
            self._smtp_factory = smtplib.SMTP
        self._config = config
        self._config_smtp_server = config.get('smtp_server')
        self._smtp_server = self._config_smtp_server
        if self._smtp_server is None:
            self._smtp_server = self._default_smtp_server

        self._smtp_username = config.get('smtp_username')
        self._smtp_password = config.get('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()
        # FIXME: _authenticate() should only be called when the server has
        # refused unauthenticated access, so it can safely try to authenticate 
        # with the default username. JRV20090407
        self._authenticate()

    def _create_connection(self):
        """Create an SMTP connection."""
        self._connection = self._smtp_factory()
        try:
            self._connection.connect(self._smtp_server)
        except socket.error, e:
            if e.args[0] == errno.ECONNREFUSED:
                if self._config_smtp_server is None:
                    raise DefaultSMTPConnectionRefused(socket.error,
                                                       self._smtp_server)
                else:
                    raise SMTPConnectionRefused(socket.error,
                                                self._smtp_server)
            else:
                raise

        # Say EHLO (falling back to HELO) to query the server's features.
        code, resp = self._connection.ehlo()
        if not (200 <= code <= 299):
            code, resp = self._connection.helo()
            if not (200 <= code <= 299):
                raise SMTPError("server refused HELO: %d %s" % (code, resp))

        # Use TLS if the server advertised it:
        if self._connection.has_extn("starttls"):
            code, resp = self._connection.starttls()
            if not (200 <= code <= 299):
                raise SMTPError("server refused STARTTLS: %d %s" % (code, resp))
            # Say EHLO again, to check for newly revealed features
            code, resp = self._connection.ehlo()
            if not (200 <= code <= 299):
                raise SMTPError("server refused EHLO: %d %s" % (code, resp))

    def _authenticate(self):
        """If necessary authenticate yourself to the server."""
        auth = config.AuthenticationConfig()
        if self._smtp_username is None:
            # FIXME: Since _authenticate gets called even when no authentication
            # is necessary, it's not possible to use the default username 
            # here yet.
            self._smtp_username = auth.get_user('smtp', self._smtp_server)
            if self._smtp_username is None:
                return

        if self._smtp_password is None:
            self._smtp_password = auth.get_password(
                'smtp', self._smtp_server, self._smtp_username)

        # smtplib requires that the username and password be byte
        # strings.  The CRAM-MD5 spec doesn't give any guidance on
        # encodings, but the SASL PLAIN spec says UTF-8, so that's
        # what we'll use.
        username = osutils.safe_utf8(self._smtp_username)
        password = osutils.safe_utf8(self._smtp_password)

        self._connection.login(username, password)

    @staticmethod
    def get_message_addresses(message):
        """Get the origin and destination addresses of a message.

        :param message: A message object supporting get() to access its
            headers, like email.Message or bzrlib.email_message.EmailMessage.
        :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.get('From', None))[1]
        to_full_addresses = []
        for header in ['To', 'Cc', 'Bcc']:
            value = message.get(header, None)
            if value:
                to_full_addresses.append(value)
        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))