13
13
# You should have received a copy of the GNU General Public License
14
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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
17
"""A convenience class around smtplib."""
19
from email import Utils
19
from email.Utils import getaddresses, parseaddr
28
from bzrlib.errors import (
31
DefaultSMTPConnectionRefused,
32
SMTPConnectionRefused,
23
from bzrlib.errors import BzrCommandError, NoDestinationAddress
36
26
class SMTPConnection(object):
66
52
self._create_connection()
67
# FIXME: _authenticate() should only be called when the server has
68
# refused unauthenticated access, so it can safely try to authenticate
69
# with the default username. JRV20090407
70
53
self._authenticate()
72
55
def _create_connection(self):
73
56
"""Create an SMTP connection."""
74
self._connection = self._smtp_factory()
76
self._connection.connect(self._smtp_server)
77
except socket.error, e:
78
if e.args[0] == errno.ECONNREFUSED:
79
if self._config_smtp_server is None:
80
raise DefaultSMTPConnectionRefused(socket.error,
83
raise SMTPConnectionRefused(socket.error,
88
# Say EHLO (falling back to HELO) to query the server's features.
89
code, resp = self._connection.ehlo()
90
if not (200 <= code <= 299):
91
code, resp = self._connection.helo()
92
if not (200 <= code <= 299):
93
raise SMTPError("server refused HELO: %d %s" % (code, resp))
95
# Use TLS if the server advertised it:
96
if self._connection.has_extn("starttls"):
97
code, resp = self._connection.starttls()
98
if not (200 <= code <= 299):
99
raise SMTPError("server refused STARTTLS: %d %s" % (code, resp))
100
# Say EHLO again, to check for newly revealed features
101
code, resp = self._connection.ehlo()
102
if not (200 <= code <= 299):
103
raise SMTPError("server refused EHLO: %d %s" % (code, resp))
57
self._connection = smtplib.SMTP()
58
self._connection.connect(self._smtp_server)
60
# If this fails, it just returns an error, but it shouldn't raise an
61
# exception unless something goes really wrong (in which case we want
63
self._connection.starttls()
105
65
def _authenticate(self):
106
66
"""If necessary authenticate yourself to the server."""
107
auth = config.AuthenticationConfig()
108
67
if self._smtp_username is None:
109
# FIXME: Since _authenticate gets called even when no authentication
110
# is necessary, it's not possible to use the default username
112
self._smtp_username = auth.get_user('smtp', self._smtp_server)
113
if self._smtp_username is None:
116
70
if self._smtp_password is None:
117
self._smtp_password = auth.get_password(
118
'smtp', self._smtp_server, self._smtp_username)
120
# smtplib requires that the username and password be byte
121
# strings. The CRAM-MD5 spec doesn't give any guidance on
122
# encodings, but the SASL PLAIN spec says UTF-8, so that's
124
username = osutils.safe_utf8(self._smtp_username)
125
password = osutils.safe_utf8(self._smtp_password)
127
self._connection.login(username, password)
71
self._smtp_password = ui.ui_factory.get_password(
72
'Please enter the SMTP password: %(user)s@%(host)s',
73
user=self._smtp_username,
74
host=self._smtp_server)
76
self._connection.login(self._smtp_username, self._smtp_password)
130
79
def get_message_addresses(message):
131
80
"""Get the origin and destination addresses of a message.
133
:param message: A message object supporting get() to access its
134
headers, like email.Message or bzrlib.email_message.EmailMessage.
82
:param message: An email.Message or email.MIMEMultipart object.
135
83
:return: A pair (from_email, to_emails), where from_email is the email
136
84
address in the From header, and to_emails a list of all the
137
85
addresses in the To, Cc, and Bcc headers.
139
from_email = Utils.parseaddr(message.get('From', None))[1]
87
from_email = parseaddr(message['From'])[1]
140
88
to_full_addresses = []
141
89
for header in ['To', 'Cc', 'Bcc']:
142
value = message.get(header, None)
144
to_full_addresses.append(value)
145
to_emails = [ pair[1] for pair in
146
Utils.getaddresses(to_full_addresses) ]
90
to_full_addresses += message.get_all(header, [])
91
to_emails = [ pair[1] for pair in getaddresses(to_full_addresses) ]
148
93
return from_email, to_emails
150
95
def send_email(self, message):
151
96
"""Send an email message.
153
The message will be sent to all addresses in the To, Cc and Bcc
98
The message will be sent to all addresses in the To, Cc and Bcc headers.
156
100
:param message: An email.Message or email.MIMEMultipart object.
166
self._connection.sendmail(from_email, to_emails,
110
self._connection.sendmail(from_email, to_emails, message.as_string())
168
111
except smtplib.SMTPRecipientsRefused, e:
169
raise SMTPError('server refused recipient: %d %s' %
170
e.recipients.values()[0])
112
raise BzrCommandError('SMTP server refused recipient: %d %s'
113
% e.recipients.values()[0])
171
114
except smtplib.SMTPResponseException, e:
172
raise SMTPError('%d %s' % (e.smtp_code, e.smtp_error))
115
raise BzrCommandError('SMTP error: %d %s' % (e.smtp_code,
173
117
except smtplib.SMTPException, e:
174
raise SMTPError(str(e))
118
raise BzrCommandError('SMTP error: ' + str(e))