1
# Copyright (C) 2007, 2010 by Canonical Ltd
2
# Authors: Robert Collins <robert.collins@canonical.com>
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
from cStringIO import StringIO
22
__version__ as _bzrlib_version,
24
from bzrlib.tests import TestCase
25
from bzrlib.plugins.email.smtp_connection import SMTPConnection
28
class InstrumentedSMTPConnection(SMTPConnection):
29
"""Instrument SMTPConnection.
31
We don't want to actually connect or send messages, so this just
35
class FakeSMTP(object):
36
"""Fakes an SMTP connection."""
38
def __init__(self, actions):
39
self.actions = actions
41
def sendmail(self, from_addr, to_addrs, msg):
42
self.actions.append(('sendmail', from_addr, to_addrs, msg))
44
def login(self, username, password):
45
self.actions.append(('login', username, password))
47
def __init__(self, config):
48
super(InstrumentedSMTPConnection, self).__init__(config)
51
def _create_connection(self):
52
self.actions.append(('create_connection',))
53
self._connection = InstrumentedSMTPConnection.FakeSMTP(self.actions)
55
def _basic_message(self, *args, **kwargs):
56
"""Override to force the boundary for easier testing."""
57
msg, from_email, to_emails = super(InstrumentedSMTPConnection,
58
self)._basic_message(*args, **kwargs)
59
msg.set_boundary('=====123456==')
60
return msg, from_email, to_emails
63
class TestSMTPConnection(TestCase):
65
def get_connection(self, text):
66
my_config = config.GlobalConfig.from_string(text)
67
return InstrumentedSMTPConnection(my_config)
69
def test_defaults(self):
70
conn = self.get_connection('')
71
self.assertEqual('localhost', conn._smtp_server)
72
self.assertEqual(None, conn._smtp_username)
73
self.assertEqual(None, conn._smtp_password)
75
def test_smtp_server(self):
76
conn = self.get_connection('[DEFAULT]\nsmtp_server=host:10\n')
77
self.assertEqual('host:10', conn._smtp_server)
79
def test_smtp_username(self):
80
conn = self.get_connection('')
81
self.assertIs(None, conn._smtp_username)
83
conn = self.get_connection('[DEFAULT]\nsmtp_username=joebody\n')
84
self.assertEqual(u'joebody', conn._smtp_username)
86
def test_smtp_password(self):
87
conn = self.get_connection('')
88
self.assertIs(None, conn._smtp_password)
90
conn = self.get_connection('[DEFAULT]\nsmtp_password=mypass\n')
91
self.assertEqual(u'mypass', conn._smtp_password)
93
def assertSplitEquals(self, username, email, address):
94
actual = SMTPConnection._split_address(address)
95
self.assertEqual((username, email), actual)
97
def test__split_address(self):
98
self.assertSplitEquals(u'Joe Foo', 'joe@foo.com',
99
u'Joe Foo <joe@foo.com>')
100
self.assertSplitEquals(u'Joe F\xb5', 'joe@foo.com',
101
u'Joe F\xb5 <joe@foo.com>')
102
self.assertSplitEquals('', 'joe', 'joe')
104
def test_simple_send(self):
105
"""Test that we build up a reasonable looking email.
107
This also tests that we extract the right email addresses, etc, and it
108
gets passed to sendmail() with the right parameters.
110
conn = self.get_connection('')
111
from_addr = u'Jerry F\xb5z <jerry@fooz.com>'
112
to_addr = u'Biz N\xe5 <biz@na.com>'
113
subject = u'Hello Biz N\xe5'
114
message=(u'Hello Biz N\xe5\n'
115
u'I haven\'t heard\n'
116
u'from you in a while\n')
117
conn.send_text_email(from_addr, [to_addr], subject, message)
118
self.assertEqual(('create_connection',), conn.actions[0])
119
self.assertEqual(('sendmail', 'jerry@fooz.com', ['biz@na.com']),
121
self.assertEqualDiff((
122
'Content-Type: multipart/mixed; boundary="=====123456=="\n'
123
'MIME-Version: 1.0\n'
124
'From: =?utf-8?q?Jerry_F=C2=B5z?= <jerry@fooz.com>\n'
125
'User-Agent: bzr/%s\n'
126
'To: =?utf-8?q?Biz_N=C3=A5?= <biz@na.com>\n'
127
'Subject: =?utf-8?q?Hello_Biz_N=C3=A5?=\n'
130
'Content-Type: text/plain; charset="utf-8"\n'
131
'MIME-Version: 1.0\n'
132
'Content-Transfer-Encoding: base64\n'
134
'SGVsbG8gQml6IE7DpQpJIGhhdmVuJ3QgaGVhcmQKZnJvbSB5b3UgaW4gYSB3aGlsZQo=\n'
137
) % _bzrlib_version, conn.actions[1][3])
139
def test_send_text_and_attachment_email(self):
140
conn = self.get_connection('')
141
from_addr = u'Jerry F\xb5z <jerry@fooz.com>'
142
to_addr = u'Biz N\xe5 <biz@na.com>'
143
subject = u'Hello Biz N\xe5'
144
message=(u'Hello Biz N\xe5\n'
145
u'See my attached patch\n')
146
diff_txt = ('=== diff contents\n'
153
conn.send_text_and_attachment_email(from_addr, [to_addr], subject,
154
message, diff_txt, 'test.diff')
155
self.assertEqual(('create_connection',), conn.actions[0])
156
self.assertEqual(('sendmail', 'jerry@fooz.com', ['biz@na.com']),
158
self.assertEqualDiff((
159
'Content-Type: multipart/mixed; boundary="=====123456=="\n'
160
'MIME-Version: 1.0\n'
161
'From: =?utf-8?q?Jerry_F=C2=B5z?= <jerry@fooz.com>\n'
162
'User-Agent: bzr/%s\n'
163
'To: =?utf-8?q?Biz_N=C3=A5?= <biz@na.com>\n'
164
'Subject: =?utf-8?q?Hello_Biz_N=C3=A5?=\n'
167
'Content-Type: text/plain; charset="utf-8"\n'
168
'MIME-Version: 1.0\n'
169
'Content-Transfer-Encoding: base64\n'
171
'SGVsbG8gQml6IE7DpQpTZWUgbXkgYXR0YWNoZWQgcGF0Y2gK\n'
174
'Content-Type: text/plain; charset="8-bit"; name="test.diff"\n'
175
'MIME-Version: 1.0\n'
176
'Content-Transfer-Encoding: base64\n'
177
'Content-Disposition: inline; filename="test.diff"\n'
179
'PT09IGRpZmYgY29udGVudHMKLS0tIG9sZAorKysgbmV3CiB1bmNoYW5nZWQKLW9sZCBiaW5hcnm1\n'
180
'Ci1uZXcgYmluYXJ55QogdW5jaGFuZ2VkCg==\n'
183
) % _bzrlib_version, conn.actions[1][3])
185
def test_create_and_send(self):
186
"""Test that you can create a custom email, and send it."""
187
conn = self.get_connection('')
188
email_msg, from_email, to_emails = conn.create_email(
189
'Joe Foo <joe@foo.com>',
190
['Jane Foo <jane@foo.com>', 'Barry Foo <barry@foo.com>'],
192
'Check out the attachment\n')
193
self.assertEqual('joe@foo.com', from_email)
194
self.assertEqual(['jane@foo.com', 'barry@foo.com'], to_emails)
198
from email.mime.nonmultipart import MIMENonMultipart
199
from email.encoders import encode_base64
202
from email.MIMENonMultipart import MIMENonMultipart
203
from email.Encoders import encode_base64
205
attachment_txt = '\x00foo\xff\xff\xff\xff'
206
attachment = MIMENonMultipart('application', 'octet-stream')
207
attachment.set_payload(attachment_txt)
208
encode_base64(attachment)
210
email_msg.attach(attachment)
212
# This will add someone to send to, but not include it in the To list.
213
to_emails.append('b@cc.com')
214
conn.send_email(email_msg, from_email, to_emails)
216
self.assertEqual(('create_connection',), conn.actions[0])
217
self.assertEqual(('sendmail', 'joe@foo.com',
218
['jane@foo.com', 'barry@foo.com', 'b@cc.com']),
220
self.assertEqualDiff((
221
'Content-Type: multipart/mixed; boundary="=====123456=="\n'
222
'MIME-Version: 1.0\n'
223
'From: Joe Foo <joe@foo.com>\n'
224
'User-Agent: bzr/%s\n'
225
'To: Jane Foo <jane@foo.com>, Barry Foo <barry@foo.com>\n'
226
'Subject: Hi Jane and Barry\n'
229
'Content-Type: text/plain; charset="utf-8"\n'
230
'MIME-Version: 1.0\n'
231
'Content-Transfer-Encoding: base64\n'
233
'Q2hlY2sgb3V0IHRoZSBhdHRhY2htZW50Cg==\n'
236
'Content-Type: application/octet-stream\n'
237
'MIME-Version: 1.0\n'
238
'Content-Transfer-Encoding: base64\n'
242
) % _bzrlib_version, conn.actions[1][3])
244
def test_email_parse(self):
245
"""Check that python's email can parse our emails."""
246
conn = self.get_connection('')
247
from_addr = u'Jerry F\xb5z <jerry@fooz.com>'
248
to_addr = u'Biz N\xe5 <biz@na.com>'
249
subject = u'Hello Biz N\xe5'
250
message=(u'Hello Biz N\xe5\n'
251
u'See my attached patch\n')
252
diff_txt = ('=== diff contents\n'
259
conn.send_text_and_attachment_email(from_addr, [to_addr], subject,
260
message, diff_txt, 'test.diff')
261
self.assertEqual(('create_connection',), conn.actions[0])
262
self.assertEqual(('sendmail', 'jerry@fooz.com', ['biz@na.com']),
264
email_message_text = conn.actions[1][3]
268
from email.parser import Parser
269
from email.header import decode_header
272
from email.Parser import Parser
273
from email.Header import decode_header
276
"""Convert a header string to a unicode string.
278
This handles '=?utf-8?q?foo=C2=B5?=' => u'Foo\\xb5'
280
return ' '.join([chunk.decode(encoding or 'ascii')
281
for chunk, encoding in decode_header(s)])
284
email_message = p.parsestr(email_message_text)
286
self.assertEqual(from_addr, decode(email_message['From']))
287
self.assertEqual(to_addr, decode(email_message['To']))
288
self.assertEqual(subject, decode(email_message['Subject']))
289
text_payload = email_message.get_payload(0)
290
diff_payload = email_message.get_payload(1)
291
# I haven't found a way to have python's email read the charset=""
292
# portion of the Content-Type header. So I'm doing it manually
293
# The 'decode=True' here means to decode from base64 => 8-bit text.
294
# text_payload.get_charset() returns None
295
text = text_payload.get_payload(decode=True).decode('utf-8')
296
self.assertEqual(message, text)
297
self.assertEqual(diff_txt, diff_payload.get_payload(decode=True))