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) |