~bzr-pqm/bzr/bzr.dev

2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
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
4183.7.1 by Sabin Iacob
update FSF mailing address
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
16
6592.1.1 by Vincent Ladeuil
Fix minor incompatible change in email python 2.7.6 module.
17
import sys
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
18
from email.Header import decode_header
19
20
from bzrlib import __version__ as _bzrlib_version
21
from bzrlib.email_message import EmailMessage
22
from bzrlib.errors import BzrBadParameterNotUnicode
23
from bzrlib.smtp_connection import SMTPConnection
6592.1.1 by Vincent Ladeuil
Fix minor incompatible change in email python 2.7.6 module.
24
from bzrlib import tests
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
25
26
EMPTY_MESSAGE = '''\
27
From: from@from.com
28
Subject: subject
29
To: to@to.com
30
User-Agent: Bazaar (%s)
31
32
''' % _bzrlib_version
33
34
_SIMPLE_MESSAGE = '''\
35
MIME-Version: 1.0
36
Content-Type: text/plain; charset="%%s"
37
Content-Transfer-Encoding: %%s
38
From: from@from.com
39
Subject: subject
40
To: to@to.com
41
User-Agent: Bazaar (%s)
42
43
%%s''' % _bzrlib_version
44
45
SIMPLE_MESSAGE_ASCII = _SIMPLE_MESSAGE % ('us-ascii', '7bit', 'body')
46
SIMPLE_MESSAGE_UTF8 = _SIMPLE_MESSAGE % ('utf-8', 'base64', 'YsOzZHk=\n')
2639.1.2 by John Arbash Meinel
Some cleanups for the EmailMessage class.
47
SIMPLE_MESSAGE_8BIT = _SIMPLE_MESSAGE % ('8-bit', 'base64', 'YvRkeQ==\n')
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
48
49
50
BOUNDARY = '=====123456=='
51
52
_MULTIPART_HEAD = '''\
53
Content-Type: multipart/mixed; boundary="%(boundary)s"
54
MIME-Version: 1.0
55
From: from@from.com
56
Subject: subject
57
To: to@to.com
58
User-Agent: Bazaar (%(version)s)
59
60
--%(boundary)s
61
MIME-Version: 1.0
62
Content-Type: text/plain; charset="us-ascii"
63
Content-Transfer-Encoding: 7bit
64
Content-Disposition: inline
65
66
body
67
''' %  { 'version': _bzrlib_version, 'boundary': BOUNDARY }
68
6592.1.1 by Vincent Ladeuil
Fix minor incompatible change in email python 2.7.6 module.
69
70
def final_newline_or_not(msg):
71
    if sys.version_info >= (2, 7, 6):
72
        # Some internals of python's email module changed in an (minor)
73
        # incompatible way: a final newline is appended in 2.7.6...
74
       msg += '\n'
75
    return msg
76
77
78
def simple_multipart_message():
79
    msg = _MULTIPART_HEAD + '--%s--' % BOUNDARY
80
    return final_newline_or_not(msg)
81
82
83
def complex_multipart_message(typ):
84
    msg = _MULTIPART_HEAD + '''\
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
85
--%(boundary)s
86
MIME-Version: 1.0
87
Content-Type: text/%%s; charset="us-ascii"; name="lines.txt"
88
Content-Transfer-Encoding: 7bit
89
Content-Disposition: inline
90
91
a
92
b
93
c
94
d
95
e
96
97
--%(boundary)s--''' %  { 'boundary': BOUNDARY }
6592.1.1 by Vincent Ladeuil
Fix minor incompatible change in email python 2.7.6 module.
98
    msg = final_newline_or_not(msg)
99
    return msg % (typ,)
100
101
102
class TestEmailMessage(tests.TestCase):
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
103
104
    def test_empty_message(self):
105
        msg = EmailMessage('from@from.com', 'to@to.com', 'subject')
106
        self.assertEqualDiff(EMPTY_MESSAGE , msg.as_string())
107
108
    def test_simple_message(self):
109
        pairs = {
110
            'body': SIMPLE_MESSAGE_ASCII,
111
            u'b\xf3dy': SIMPLE_MESSAGE_UTF8,
112
            'b\xc3\xb3dy': SIMPLE_MESSAGE_UTF8,
2639.1.2 by John Arbash Meinel
Some cleanups for the EmailMessage class.
113
            'b\xf4dy': SIMPLE_MESSAGE_8BIT,
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
114
        }
115
        for body, expected in pairs.items():
116
            msg = EmailMessage('from@from.com', 'to@to.com', 'subject', body)
117
            self.assertEqualDiff(expected, msg.as_string())
118
6592.1.1 by Vincent Ladeuil
Fix minor incompatible change in email python 2.7.6 module.
119
    def test_multipart_message_simple(self):
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
120
        msg = EmailMessage('from@from.com', 'to@to.com', 'subject')
121
        msg.add_inline_attachment('body')
6592.1.1 by Vincent Ladeuil
Fix minor incompatible change in email python 2.7.6 module.
122
        self.assertEqualDiff(simple_multipart_message(),
123
                             msg.as_string(BOUNDARY))
124
125
126
    def test_multipart_message_complex(self):
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
127
        msg = EmailMessage('from@from.com', 'to@to.com', 'subject', 'body')
128
        msg.add_inline_attachment(u'a\nb\nc\nd\ne\n', 'lines.txt', 'x-subtype')
6592.1.1 by Vincent Ladeuil
Fix minor incompatible change in email python 2.7.6 module.
129
        self.assertEqualDiff(complex_multipart_message('x-subtype'),
130
                             msg.as_string(BOUNDARY))
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
131
132
    def test_headers_accept_unicode_and_utf8(self):
2625.6.3 by Adeodato Simó
Changes after review by John.
133
        for user in [ u'Pepe P\xe9rez <pperez@ejemplo.com>',
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
134
                'Pepe P\xc3\xa9red <pperez@ejemplo.com>' ]:
2625.6.3 by Adeodato Simó
Changes after review by John.
135
            msg = EmailMessage(user, user, user) # no exception raised
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
136
137
            for header in ['From', 'To', 'Subject']:
138
                value = msg[header]
139
                str(value).decode('ascii') # no UnicodeDecodeError
140
141
    def test_headers_reject_8bit(self):
142
        for i in range(3): # from_address, to_address, subject
143
            x = [ '"J. Random Developer" <jrandom@example.com>' ] * 3
144
            x[i] = 'Pepe P\xe9rez <pperez@ejemplo.com>'
145
            self.assertRaises(BzrBadParameterNotUnicode, EmailMessage, *x)
146
147
    def test_multiple_destinations(self):
148
        to_addresses = [ 'to1@to.com', 'to2@to.com', 'to3@to.com' ]
149
        msg = EmailMessage('from@from.com', to_addresses, 'subject')
150
        self.assertContainsRe(msg.as_string(), 'To: ' +
151
                ', '.join(to_addresses)) # re.M can't be passed, so no ^$
152
153
    def test_retrieving_headers(self):
154
        msg = EmailMessage('from@from.com', 'to@to.com', 'subject')
155
        for header, value in [('From', 'from@from.com'), ('To', 'to@to.com'),
156
                ('Subject', 'subject')]:
157
            self.assertEqual(value, msg.get(header))
158
            self.assertEqual(value, msg[header])
159
        self.assertEqual(None, msg.get('Does-Not-Exist'))
160
        self.assertEqual(None, msg['Does-Not-Exist'])
161
        self.assertEqual('None', msg.get('Does-Not-Exist', 'None'))
162
163
    def test_setting_headers(self):
164
        msg = EmailMessage('from@from.com', 'to@to.com', 'subject')
165
        msg['To'] = 'to2@to.com'
166
        msg['Cc'] = 'cc@cc.com'
167
        self.assertEqual('to2@to.com', msg['To'])
168
        self.assertEqual('cc@cc.com', msg['Cc'])
169
170
    def test_address_to_encoded_header(self):
171
        def decode(s):
172
            """Convert a RFC2047-encoded string to a unicode string."""
173
            return ' '.join([chunk.decode(encoding or 'ascii')
174
                             for chunk, encoding in decode_header(s)])
175
176
        address = 'jrandom@example.com'
177
        encoded = EmailMessage.address_to_encoded_header(address)
178
        self.assertEqual(address, encoded)
179
180
        address = 'J Random Developer <jrandom@example.com>'
181
        encoded = EmailMessage.address_to_encoded_header(address)
182
        self.assertEqual(address, encoded)
183
184
        address = '"J. Random Developer" <jrandom@example.com>'
185
        encoded = EmailMessage.address_to_encoded_header(address)
186
        self.assertEqual(address, encoded)
187
188
        address = u'Pepe P\xe9rez <pperez@ejemplo.com>' # unicode ok
189
        encoded = EmailMessage.address_to_encoded_header(address)
190
        self.assert_('pperez@ejemplo.com' in encoded) # addr must be unencoded
191
        self.assertEquals(address, decode(encoded))
192
193
        address = 'Pepe P\xc3\xa9red <pperez@ejemplo.com>' # UTF-8 ok
194
        encoded = EmailMessage.address_to_encoded_header(address)
195
        self.assert_('pperez@ejemplo.com' in encoded)
196
        self.assertEquals(address, decode(encoded).encode('utf-8'))
197
198
        address = 'Pepe P\xe9rez <pperez@ejemplo.com>' # ISO-8859-1 not ok
199
        self.assertRaises(BzrBadParameterNotUnicode,
200
                EmailMessage.address_to_encoded_header, address)
201
202
    def test_string_with_encoding(self):
203
        pairs = {
204
                u'Pepe':        ('Pepe', 'ascii'),
205
                u'P\xe9rez':    ('P\xc3\xa9rez', 'utf-8'),
206
                'Perez':         ('Perez', 'ascii'), # u'Pepe' == 'Pepe'
207
                'P\xc3\xa9rez': ('P\xc3\xa9rez', 'utf-8'),
2639.1.2 by John Arbash Meinel
Some cleanups for the EmailMessage class.
208
                'P\xe8rez':     ('P\xe8rez', '8-bit'),
2625.6.1 by Adeodato Simó
New EmailMessage class, façade around email.Message and MIMEMultipart.
209
        }
2625.6.3 by Adeodato Simó
Changes after review by John.
210
        for string_, pair in pairs.items():
211
            self.assertEqual(pair, EmailMessage.string_with_encoding(string_))
6592.1.1 by Vincent Ladeuil
Fix minor incompatible change in email python 2.7.6 module.
212
213
214
class TestSend(tests.TestCase):
215
216
    def setUp(self):
217
        super(TestSend, self).setUp()
218
        self.messages = []
219
220
        def send_as_append(_self, msg):
221
            self.messages.append(msg.as_string(BOUNDARY))
222
223
        self.overrideAttr(SMTPConnection, 'send_email', send_as_append)
224
225
226
227
    def send_email(self, attachment=None, attachment_filename=None,
228
                   attachment_mime_subtype='plain'):
229
        class FakeConfig:
230
            def get(self, option):
231
                return None
232
233
        EmailMessage.send(FakeConfig(), 'from@from.com', 'to@to.com',
234
                          'subject', 'body',
235
                          attachment=attachment,
236
                          attachment_filename=attachment_filename,
237
                          attachment_mime_subtype=attachment_mime_subtype)
238
239
    def assertMessage(self, expected):
240
        self.assertLength(1, self.messages)
241
        self.assertEqualDiff(expected, self.messages[0])
242
243
    def test_send_plain(self):
244
        self.send_email(u'a\nb\nc\nd\ne\n', 'lines.txt')
245
        self.assertMessage(complex_multipart_message('plain'))
246
247
    def test_send_patch(self):
248
        self.send_email(u'a\nb\nc\nd\ne\n', 'lines.txt', 'x-patch')
249
        self.assertMessage(complex_multipart_message('x-patch'))
250
251
    def test_send_simple(self):
252
          self.send_email()
253
          self.assertMessage(SIMPLE_MESSAGE_ASCII)
254