1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
18 # $Id: test_multipart.py,v 1.8 2007-09-22 07:25:35 jpend Exp $
20 import unittest
21 from cStringIO import StringIO
23 from roundup.mailgw import Message
25 class TestMessage(Message):
26 # A note on message/rfc822: The content of such an attachment is an
27 # email with at least one header line. RFC2046 tells us: """ A
28 # media type of "message/rfc822" indicates that the body contains an
29 # encapsulated message, with the syntax of an RFC 822 message.
30 # However, unlike top-level RFC 822 messages, the restriction that
31 # each "message/rfc822" body must include a "From", "Date", and at
32 # least one destination header is removed and replaced with the
33 # requirement that at least one of "From", "Subject", or "Date" must
34 # be present."""
35 # This means we have to add a newline after the mime-header before
36 # the subject, otherwise the subject is part of the mime header not
37 # part of the email header.
38 table = {'multipart/signed': ' boundary="boundary-%(indent)s";\n',
39 'multipart/mixed': ' boundary="boundary-%(indent)s";\n',
40 'multipart/alternative': ' boundary="boundary-%(indent)s";\n',
41 'text/plain': ' name="foo.txt"\nfoo\n',
42 'application/pgp-signature': ' name="foo.gpg"\nfoo\n',
43 'application/pdf': ' name="foo.pdf"\nfoo\n',
44 'message/rfc822': '\nSubject: foo\n\nfoo\n'}
46 def __init__(self, spec):
47 """Create a basic MIME message according to 'spec'.
49 Each line of a spec has one content-type, which is optionally indented.
50 The indentation signifies how deep in the MIME hierarchy the
51 content-type is.
53 """
54 parts = []
55 for line in spec.splitlines():
56 content_type = line.strip()
57 if not content_type:
58 continue
60 indent = self.getIndent(line)
61 if indent:
62 parts.append('\n--boundary-%s\n' % indent)
63 parts.append('Content-type: %s;\n' % content_type)
64 parts.append(self.table[content_type] % {'indent': indent + 1})
66 Message.__init__(self, StringIO(''.join(parts)))
68 def getIndent(self, line):
69 """Get the current line's indentation, using four-space indents."""
70 count = 0
71 for char in line:
72 if char != ' ':
73 break
74 count += 1
75 return count / 4
77 class MultipartTestCase(unittest.TestCase):
78 def setUp(self):
79 self.fp = StringIO()
80 w = self.fp.write
81 w('Content-Type: multipart/mixed; boundary="foo"\r\n\r\n')
82 w('This is a multipart message. Ignore this bit.\r\n')
83 w('\r\n--foo\r\n')
85 w('Content-Type: text/plain\r\n\r\n')
86 w('Hello, world!\r\n')
87 w('\r\n')
88 w('Blah blah\r\n')
89 w('foo\r\n')
90 w('-foo\r\n')
91 w('\r\n--foo\r\n')
93 w('Content-Type: multipart/alternative; boundary="bar"\r\n\r\n')
94 w('This is a multipart message. Ignore this bit.\r\n')
95 w('\r\n--bar\r\n')
97 w('Content-Type: text/plain\r\n\r\n')
98 w('Hello, world!\r\n')
99 w('\r\n')
100 w('Blah blah\r\n')
101 w('\r\n--bar\r\n')
103 w('Content-Type: text/html\r\n\r\n')
104 w('<b>Hello, world!</b>\r\n')
105 w('\r\n--bar--\r\n')
106 w('\r\n--foo\r\n')
108 w('Content-Type: text/plain\r\n\r\n')
109 w('Last bit\n')
110 w('\r\n--foo--\r\n')
111 self.fp.seek(0)
113 def testMultipart(self):
114 m = Message(self.fp)
115 self.assert_(m is not None)
117 # skip the first bit
118 p = m.getpart()
119 self.assert_(p is not None)
120 self.assertEqual(p.fp.read(),
121 'This is a multipart message. Ignore this bit.\r\n')
123 # first text/plain
124 p = m.getpart()
125 self.assert_(p is not None)
126 self.assertEqual(p.gettype(), 'text/plain')
127 self.assertEqual(p.fp.read(),
128 'Hello, world!\r\n\r\nBlah blah\r\nfoo\r\n-foo\r\n')
130 # sub-multipart
131 p = m.getpart()
132 self.assert_(p is not None)
133 self.assertEqual(p.gettype(), 'multipart/alternative')
135 # sub-multipart text/plain
136 q = p.getpart()
137 self.assert_(q is not None)
138 q = p.getpart()
139 self.assert_(q is not None)
140 self.assertEqual(q.gettype(), 'text/plain')
141 self.assertEqual(q.fp.read(), 'Hello, world!\r\n\r\nBlah blah\r\n')
143 # sub-multipart text/html
144 q = p.getpart()
145 self.assert_(q is not None)
146 self.assertEqual(q.gettype(), 'text/html')
147 self.assertEqual(q.fp.read(), '<b>Hello, world!</b>\r\n')
149 # sub-multipart end
150 q = p.getpart()
151 self.assert_(q is None)
153 # final text/plain
154 p = m.getpart()
155 self.assert_(p is not None)
156 self.assertEqual(p.gettype(), 'text/plain')
157 self.assertEqual(p.fp.read(),
158 'Last bit\n')
160 # end
161 p = m.getpart()
162 self.assert_(p is None)
164 def TestExtraction(self, spec, expected):
165 self.assertEqual(TestMessage(spec).extract_content(), expected)
167 def testTextPlain(self):
168 self.TestExtraction('text/plain', ('foo\n', []))
170 def testAttachedTextPlain(self):
171 self.TestExtraction("""
172 multipart/mixed
173 text/plain
174 text/plain""",
175 ('foo\n',
176 [('foo.txt', 'text/plain', 'foo\n')]))
178 def testMultipartMixed(self):
179 self.TestExtraction("""
180 multipart/mixed
181 text/plain
182 application/pdf""",
183 ('foo\n',
184 [('foo.pdf', 'application/pdf', 'foo\n')]))
186 def testMultipartAlternative(self):
187 self.TestExtraction("""
188 multipart/alternative
189 text/plain
190 application/pdf
191 """, ('foo\n', [('foo.pdf', 'application/pdf', 'foo\n')]))
193 def testDeepMultipartAlternative(self):
194 self.TestExtraction("""
195 multipart/mixed
196 multipart/alternative
197 text/plain
198 application/pdf
199 """, ('foo\n', [('foo.pdf', 'application/pdf', 'foo\n')]))
201 def testSignedText(self):
202 self.TestExtraction("""
203 multipart/signed
204 text/plain
205 application/pgp-signature""", ('foo\n', []))
207 def testSignedAttachments(self):
208 self.TestExtraction("""
209 multipart/signed
210 multipart/mixed
211 text/plain
212 application/pdf
213 application/pgp-signature""",
214 ('foo\n',
215 [('foo.pdf', 'application/pdf', 'foo\n')]))
217 def testAttachedSignature(self):
218 self.TestExtraction("""
219 multipart/mixed
220 text/plain
221 application/pgp-signature""",
222 ('foo\n',
223 [('foo.gpg', 'application/pgp-signature', 'foo\n')]))
225 def testMessageRfc822(self):
226 self.TestExtraction("""
227 multipart/mixed
228 message/rfc822""",
229 (None,
230 [('foo.eml', 'message/rfc822', 'Subject: foo\n\nfoo\n')]))
232 def test_suite():
233 suite = unittest.TestSuite()
234 suite.addTest(unittest.makeSuite(MultipartTestCase))
235 return suite
237 if __name__ == '__main__':
238 runner = unittest.TextTestRunner()
239 unittest.main(testRunner=runner)
242 # vim: set filetype=python ts=4 sw=4 et si