mime 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/README +256 -0
  2. data/Rakefile +34 -0
  3. data/lib/mime.rb +32 -0
  4. data/lib/mime/composite_media_type.rb +169 -0
  5. data/lib/mime/content_types.rb +96 -0
  6. data/lib/mime/discrete_media_factory.rb +69 -0
  7. data/lib/mime/discrete_media_type.rb +79 -0
  8. data/lib/mime/error.rb +32 -0
  9. data/lib/mime/header_container.rb +34 -0
  10. data/lib/mime/headers/internet.rb +90 -0
  11. data/lib/mime/headers/mime.rb +118 -0
  12. data/lib/mime/media_type.rb +45 -0
  13. data/lib/mime/message.rb +51 -0
  14. data/lib/mime/parser.rb +16 -0
  15. data/test/mime_test.rb +386 -0
  16. data/test/scaffold/application.msg +8 -0
  17. data/test/scaffold/audio.msg +8 -0
  18. data/test/scaffold/book.pdf +0 -0
  19. data/test/scaffold/data.xml +17 -0
  20. data/test/scaffold/image.jpg +0 -0
  21. data/test/scaffold/image.msg +0 -0
  22. data/test/scaffold/index.html +6 -0
  23. data/test/scaffold/main.css +0 -0
  24. data/test/scaffold/mini.mov +0 -0
  25. data/test/scaffold/multipart_alternative.msg +17 -0
  26. data/test/scaffold/multipart_alternative_related.msg +0 -0
  27. data/test/scaffold/multipart_form_data_file.msg +0 -0
  28. data/test/scaffold/multipart_form_data_file_and_text.msg +0 -0
  29. data/test/scaffold/multipart_form_data_mixed.msg +0 -0
  30. data/test/scaffold/multipart_form_data_text.msg +40 -0
  31. data/test/scaffold/multipart_mixed_inline_and_attachment.msg +0 -0
  32. data/test/scaffold/multipart_mixed_inline_and_attachment2.msg +0 -0
  33. data/test/scaffold/multipart_related.msg +0 -0
  34. data/test/scaffold/plain_text_email.msg +9 -0
  35. data/test/scaffold/ruby.png +0 -0
  36. data/test/scaffold/song.mp3 +0 -0
  37. data/test/scaffold/text.msg +7 -0
  38. data/test/scaffold/unknown.yyy +1 -0
  39. data/test/scaffold/video.msg +8 -0
  40. metadata +92 -0
@@ -0,0 +1,45 @@
1
+ require 'mime/error'
2
+ require 'mime/header_container'
3
+ require 'mime/headers/mime'
4
+
5
+ module MIME
6
+
7
+ #
8
+ # Abstract top-level media type class.
9
+ #
10
+ class MediaType
11
+
12
+ include Headers::MIME
13
+
14
+ attr_reader :headers
15
+ attr_accessor :body
16
+ protected :body, :body=
17
+
18
+ def initialize body, content_type
19
+ AbstractClassError.no_instantiation(self, MediaType)
20
+
21
+ @headers = HeaderContainer.new
22
+ @body = body
23
+ self.content_id = unique_id
24
+ self.content_type = content_type
25
+ end
26
+
27
+ #
28
+ # Transform the the MediaType object into a MIME message.
29
+ #
30
+ def to_s
31
+ "#{headers}\r\n\r\n#{body}"
32
+ end
33
+
34
+ private
35
+
36
+ #
37
+ # Generate a globally unique identifier for use in boundaries and IDs.
38
+ #
39
+ def unique_id
40
+ "#{object_id.abs}#{rand}"
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,51 @@
1
+ require 'time'
2
+ require 'mime/headers/internet'
3
+ require 'mime/headers/mime'
4
+
5
+
6
+ module MIME
7
+
8
+ #
9
+ # Construct textual messages using the RFC 2822 Internet message format.
10
+ #
11
+ class Message
12
+
13
+ include Headers::Internet
14
+ include Headers::MIME
15
+
16
+ # HeaderContainer access
17
+ attr_reader :headers
18
+
19
+ attr_accessor :body
20
+
21
+ #
22
+ # Return a Message object with body optionally set to +body+.
23
+ #
24
+ def initialize body = nil
25
+ @body = body
26
+ @headers = HeaderContainer.new
27
+ self.date = Time.now.rfc2822
28
+ self.message_id = "#{rand(1E9)}@#{__id__.abs}"
29
+ self.mime_version = "1.0 (Ruby MIME v#{VERSION})"
30
+ end
31
+
32
+ #
33
+ # Return the Internet message formatted representation of the instance.
34
+ #
35
+ def to_s
36
+ #--
37
+ # In an RFC 2822 message, the header and body sections must be separated
38
+ # by two line breaks (CRLF). One line break is deliberately missing,
39
+ # allowing a body supplier to append headers to the top-level message
40
+ # header section. Consequently, the body supplier is responsible for
41
+ # handling the body-header separation. Furthermore, if the +body+ is
42
+ # empty, the header section will be properly terminated, creating a
43
+ # standards compliant message.
44
+ #++
45
+
46
+ "#{headers}\r\n#{body}\r\n"
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,16 @@
1
+ module MIME
2
+
3
+ #
4
+ # Parse MIME messages, constructing MediaType objects for each MIME entity.
5
+ #
6
+ # TODO Implement
7
+ #
8
+ class Parser
9
+
10
+ def initialize
11
+ raise NotImplementedError.new('building a parser is a TODO')
12
+ end
13
+
14
+ end
15
+
16
+ end
@@ -0,0 +1,386 @@
1
+ require 'mime'
2
+ require 'test/unit'
3
+
4
+ class MIMETest < Test::Unit::TestCase
5
+
6
+ include MIME
7
+
8
+ def test_make_top_level_rfc2822_message
9
+ data = "\r\nmessage body"
10
+ rfc2822_msg = Message.new
11
+ rfc2822_msg.body = data
12
+ expected_msg =
13
+ "Message-ID: <993708956@989739608>\r\n"\
14
+ "Date: Wed, 24 Oct 2008 15:45:31 -0700\r\n"\
15
+ "MIME-Version: 1.0 (Ruby MIME v0.1)\r\n"\
16
+ "\r\nmessage body\r\n"
17
+
18
+ assert_equal_mime_message expected_msg, rfc2822_msg.to_s
19
+ end
20
+
21
+ def test_make_audio_message
22
+ audio = '0110000101110101011001000110100101101111'
23
+ audio_media = AudioMedia.new(audio, 'audio/midi')
24
+ audio_media.content_transfer_encoding = 'binary'
25
+
26
+ mime_msg = Message.new(audio_media).to_s
27
+ expected_mime_msg = IO.read(sd('/audio.msg'))
28
+
29
+ assert_equal_mime_message expected_mime_msg, mime_msg
30
+ end
31
+
32
+ def test_make_application_message
33
+ application = '011000100110100101101110011000010111001001111001'
34
+ application_media = ApplicationMedia.new(application)
35
+ application_media.content_transfer_encoding = 'binary'
36
+
37
+ mime_msg = Message.new(application_media).to_s
38
+ expected_mime_msg = IO.read(sd('/application.msg'))
39
+
40
+ assert_equal_mime_message expected_mime_msg, mime_msg
41
+ end
42
+
43
+ def test_make_image_message
44
+ image = IO.read(sd('/image.jpg'))
45
+ image_media = ImageMedia.new(image)
46
+ image_media.content_type = 'image/jpeg'
47
+ image_media.content_transfer_encoding = 'binary'
48
+
49
+ mime_msg = Message.new(image_media).to_s
50
+ expected_mime_msg = IO.read(sd('/image.msg'))
51
+
52
+ assert_equal_mime_message expected_mime_msg, mime_msg
53
+ end
54
+
55
+ def test_make_text_message
56
+ text = 'a plain text message'
57
+ mime_msg = Message.new(TextMedia.new(text)).to_s
58
+ expected_mime_msg = IO.read(sd('/text.msg'))
59
+
60
+ assert_equal_mime_message expected_mime_msg, mime_msg
61
+ end
62
+
63
+ def test_make_video_message
64
+ video = '0111011001101001011001000110010101101111'
65
+ video_media = VideoMedia.new(video)
66
+ video_media.content_type = 'video/mpeg'
67
+ video_media.content_transfer_encoding = 'binary'
68
+
69
+ mime_msg = Message.new(video_media).to_s
70
+ expected_mime_msg = IO.read(sd('/video.msg'))
71
+
72
+ assert_equal_mime_message expected_mime_msg, mime_msg
73
+ end
74
+
75
+ def test_no_instantiation_of_abstract_classes
76
+ e = AbstractClassError
77
+ assert_raise(e) {MediaType.new(nil, nil)}
78
+ assert_raise(e) {DiscreteMediaType.new(nil)}
79
+ assert_raise(e) {CompositeMediaType.new(nil)}
80
+ assert_raise(e) {MultipartMedia.new(nil)}
81
+ end
82
+
83
+ def test_multipart_form_data_with_text_entities
84
+ t1 = TextMedia.new('this is t1')
85
+ t2 = TextMedia.new('this is t2', 'text/enhanced')
86
+ t3 = open(sd('/index.html')) {|f| TextMedia.new(f.read, 'text/html')}
87
+ t4 = open(sd('/data.xml')) {|f| TextMedia.new(f.read, 'text/xml')}
88
+
89
+ form_data = MultipartMedia::FormData.new
90
+ form_data.add_entity t1, 'txt1'
91
+ form_data.add_entity t2, 'txt2'
92
+ form_data.add_entity t3, 'txt3'
93
+ form_data.add_entity t3, 'txt3'
94
+
95
+ expected = IO.read(sd('/multipart_form_data_text.msg'))
96
+
97
+ assert_equal_mime_message expected, form_data.to_s
98
+ end
99
+
100
+ def test_multipart_form_data_with_file_entities
101
+ img1 = sd('/image.jpg')
102
+ img2 = sd('/ruby.png')
103
+ f1 = open(img1) {|f| ImageMedia.new(f.read, 'image/jpeg')}
104
+ f2 = open(img2) {|f| ImageMedia.new(f.read, 'image/png')}
105
+
106
+ f1.content_transfer_encoding = 'binary'
107
+ f2.content_transfer_encoding = 'binary'
108
+
109
+ form_data = MultipartMedia::FormData.new
110
+ form_data.add_entity f1, 'file1', img1
111
+ form_data.add_entity f2, 'file2', img2
112
+
113
+ expected = IO.read(sd('/multipart_form_data_file.msg'))
114
+
115
+ assert_equal_mime_message expected, form_data.to_s
116
+ end
117
+
118
+ def test_multipart_form_data_with_file_and_text_entities
119
+ t1 = TextMedia.new('this is t1')
120
+ t2 = TextMedia.new('this is t2', 'text/enhanced')
121
+
122
+ img1 = sd('/image.jpg')
123
+ f1 = open(img1) {|f| ImageMedia.new(f.read, 'image/jpeg')}
124
+ f1.content_transfer_encoding = 'binary'
125
+
126
+ form_data = MultipartMedia::FormData.new
127
+ form_data.add_entity t1, 'txt1'
128
+ form_data.add_entity t2, 'txt2'
129
+ form_data.add_entity f1, 'img1', img1
130
+
131
+ expected = IO.read(sd('/multipart_form_data_file_and_text.msg'))
132
+
133
+ assert_equal_mime_message expected, form_data.to_s
134
+ end
135
+
136
+ def test_construction_of_plain_text_email_message
137
+ email_msg = Message.new
138
+ email_msg.to = {
139
+ 'person1@domain.com' => 'Harry',
140
+ 'person2@domain.com' => nil,
141
+ 'person3@domain.com' => 'Mary'
142
+ }
143
+ email_msg.cc = {'Head Honcho' => 'bossman@domain.com'}
144
+ email_msg.from = 'person4@domain.com'
145
+ email_msg.subject = 'This is an important email'
146
+ #TODO - what do we do about this body thing???????? raw_body= and body=
147
+ email_msg.body = "\r\nThis is the all important email body"
148
+ #email_msg.body = TextMedia.new("This is the all important email body")
149
+
150
+ expected = IO.read(sd('/plain_text_email.msg'))
151
+
152
+ assert_equal_mime_message expected, email_msg.to_s
153
+ end
154
+
155
+ def test_content_type_detection
156
+ (o = Object.new).extend(ContentTypes)
157
+
158
+ # test using file path
159
+ assert_equal 'application/pdf', o.file_type('book.pdf')
160
+ assert_equal 'audio/mpeg', o.file_type('/tmp/song.mp3')
161
+ assert_equal 'text/css', o.file_type('/tmp/main.css')
162
+ assert_equal 'video/quicktime', o.file_type('mini.mov')
163
+ assert_equal 'application/octet-stream', o.file_type('dsk.iso')
164
+ assert_equal nil, o.file_type('file.yyy')
165
+
166
+ # test using file object
167
+ img_file = sd('/ruby.png')
168
+ img_type = open(img_file) {|f| o.file_type(f)}
169
+ assert_equal 'image/png', img_type
170
+ assert_not_equal 'image/jpeg', img_type
171
+ end
172
+
173
+ def test_object_instantiation_using_discrete_media_factory
174
+ application_file = sd('/book.pdf')
175
+ audio_file = sd('/song.mp3')
176
+ text_file = sd('/data.xml')
177
+ video_file = sd('/mini.mov')
178
+ image_file = sd('/image.jpg')
179
+ unknown_file = sd('/unknown.yyy')
180
+
181
+ dmf = DiscreteMediaFactory
182
+
183
+ # test using file path
184
+ assert_kind_of ApplicationMedia, dmf.create(application_file)
185
+ assert_kind_of AudioMedia, dmf.create(audio_file)
186
+ assert_kind_of TextMedia, dmf.create(text_file)
187
+ assert_kind_of VideoMedia, dmf.create(video_file)
188
+
189
+ # test using file object
190
+ open(image_file) do |image_file_obj|
191
+ assert_kind_of ImageMedia, dmf.create(image_file_obj)
192
+ end
193
+ open(text_file) do |text_file_obj|
194
+ assert_kind_of TextMedia, dmf.create(text_file_obj)
195
+ end
196
+
197
+ # raise for unknown file path and File object
198
+ assert_raises(UnknownContentError) {dmf.create(unknown_file)}
199
+ open(unknown_file) do |unknown_file_obj|
200
+ assert_raises(UnknownContentError) {dmf.create(unknown_file_obj)}
201
+ end
202
+ end
203
+
204
+ def test_discrete_media_factory_creates_path_singleton_method
205
+ pdf_file_path = sd('/book.pdf')
206
+
207
+ media_obj = DiscreteMediaFactory.create(pdf_file_path)
208
+ assert_equal pdf_file_path, media_obj.path
209
+
210
+ open(pdf_file_path) do |pdf_file_obj|
211
+ media_obj = DiscreteMediaFactory.create(pdf_file_obj)
212
+ end
213
+ assert_equal pdf_file_path, media_obj.path
214
+ end
215
+
216
+ def test_multipart_alternative_message_construction
217
+ txt_data = "*Header*\nmessage\n"
218
+ htm_data = "<html><body><h1>Header</h1><p>message</p></body></html>\n"
219
+ txt_msg = TextMedia.new(txt_data)
220
+ htm_msg = TextMedia.new(htm_data)
221
+
222
+ txt_msg.content_type = 'text/plain; charset=us-ascii'
223
+ htm_msg.content_type = 'text/html; charset=iso-8859-1'
224
+
225
+ alt_msg = MultipartMedia::Alternative.new
226
+ alt_msg.add_entity htm_msg
227
+ alt_msg.add_entity txt_msg
228
+
229
+ expected = IO.read(sd('/multipart_alternative.msg'))
230
+
231
+ assert_equal_mime_message expected, alt_msg.to_s
232
+ end
233
+
234
+ def test_multipart_mixed_with_inline_and_attachment
235
+ mixed_msg = MultipartMedia::Mixed.new
236
+
237
+ open(sd('/image.jpg')) do |img_file|
238
+ img_msg = ImageMedia.new(img_file.read, 'image/jpeg')
239
+ mixed_msg.attach_entity(img_msg, 'filename' => img_file.path)
240
+ end
241
+ mixed_msg.inline_entity(TextMedia.new('This is plain text'))
242
+
243
+ expected = IO.read(sd('/multipart_mixed_inline_and_attachment.msg'))
244
+
245
+ assert_equal_mime_message expected, mixed_msg.to_s
246
+ end
247
+
248
+ def test_multipart_mixed_message_construction_using_media_factory
249
+ img1 = sd('/image.jpg')
250
+ img2 = sd('/ruby.png')
251
+ txt = sd('/index.html')
252
+ bot_img = DiscreteMediaFactory.create(img1)
253
+ top_img = DiscreteMediaFactory.create(img2)
254
+ top_txt = DiscreteMediaFactory.create(txt)
255
+
256
+ mixed_msg = MultipartMedia::Mixed.new
257
+ mixed_msg.attach_entity(bot_img)
258
+ mixed_msg.attach_entity(top_img)
259
+ mixed_msg.inline_entity(top_txt)
260
+
261
+ expected = IO.read(sd('/multipart_mixed_inline_and_attachment2.msg'))
262
+
263
+ assert_equal_mime_message expected, mixed_msg.to_s
264
+ end
265
+
266
+ def test_multipart_form_data_with_mixed_entity
267
+ txt = TextMedia.new('Joe Blow')
268
+ img1 = DiscreteMediaFactory.create(sd('/image.jpg'))
269
+ img2 = DiscreteMediaFactory.create(sd('/ruby.png'))
270
+
271
+ mixed_msg = MultipartMedia::Mixed.new
272
+ mixed_msg.attach_entity(img2)
273
+ mixed_msg.attach_entity(img1)
274
+
275
+ form = MultipartMedia::FormData.new
276
+ form.add_entity(mixed_msg, 'pics')
277
+ form.add_entity(txt, 'field1')
278
+
279
+ # similar to example 6 in RFC1867
280
+ expected = IO.read(sd('/multipart_form_data_mixed.msg'))
281
+
282
+ assert_equal_mime_message expected, form.to_s
283
+ end
284
+
285
+ def test_multipart_related_html_message_with_embedded_image
286
+ img = DiscreteMediaFactory.create(sd('/ruby.png'))
287
+ img.content_transfer_encoding = 'binary'
288
+
289
+ html_msg = TextMedia.new(<<-html, 'text/html; charset=iso-8859-1')
290
+ <html>
291
+ <body>
292
+ <h1>HTML multipart/related message</h1>
293
+ <p>txt before pix</p>
294
+ <img alt="cool ruby" src="cid:#{img.content_id}"/>
295
+ <p>txt after pix</p>
296
+ </body>
297
+ </html>
298
+ html
299
+ html_msg.content_transfer_encoding = '7bit'
300
+
301
+ related_msg = MultipartMedia::Related.new
302
+ related_msg.inline_entity(img)
303
+ related_msg.add_entity(html_msg)
304
+
305
+ expected = IO.read(sd('/multipart_related.msg'))
306
+
307
+ assert_equal_mime_message expected, related_msg.to_s
308
+ end
309
+
310
+ def test_multipart_alternative_with_related_html_entity
311
+ img = DiscreteMediaFactory.create(sd('/ruby.png'))
312
+ img.content_transfer_encoding = 'binary'
313
+
314
+ html_msg = TextMedia.new(<<-html, 'text/html; charset=iso-8859-1')
315
+ <html>
316
+ <body>
317
+ <h1>HTML multipart/alternative message</h1>
318
+ <p>txt before pix</p>
319
+ <img alt="cool ruby" src="cid:#{img.content_id}"/>
320
+ <p>txt after pix</p>
321
+ </body>
322
+ </html>
323
+ html
324
+ html_msg.content_transfer_encoding = '7bit'
325
+
326
+ text_msg = TextMedia.new(<<-text)
327
+ *HTML multipart/alternative message*
328
+ txt before pix
329
+ <cool ruby image>
330
+ txt after pix
331
+ text
332
+ text_msg.content_transfer_encoding = '7bit'
333
+
334
+ related_msg = MultipartMedia::Related.new
335
+ related_msg.inline_entity(img)
336
+ related_msg.add_entity(html_msg)
337
+
338
+ alt_msg = MultipartMedia::Alternative.new
339
+ alt_msg.add_entity(related_msg)
340
+ alt_msg.add_entity(text_msg)
341
+
342
+ expected = IO.read(sd('/multipart_alternative_related.msg'))
343
+
344
+ assert_equal_mime_message expected, alt_msg.to_s
345
+ end
346
+
347
+
348
+ private
349
+
350
+ #
351
+ # Test the equality of the normalized +expected+ and +actual+ MIME messages.
352
+ #
353
+ def assert_equal_mime_message expected, actual
354
+ assert_equal normalize_message(expected), normalize_message(actual)
355
+ end
356
+
357
+ #
358
+ # Make messages comparable by removing *-ID header values, library version
359
+ # comment in MIME-Version header, Date header value, multipart/related
360
+ # content IDs, and boundaries.
361
+ #
362
+ def normalize_message message
363
+ # these are very delicate REs that are inter-dependent, be careful
364
+ match_id_headers = /-ID: <[^>]+>\r\n/
365
+ match_boundaries = /Boundary_\d+\.\d+/
366
+ match_lib_version = / \(Ruby MIME v\d\.\d\)/
367
+ match_date_header = /^Date: .*\d{4}\r\n/
368
+ match_related_cid = /cid:\d+\.\d+/
369
+
370
+ message.
371
+ gsub(match_related_cid, "cid").
372
+ gsub(match_id_headers, "-ID:\r\n").
373
+ gsub(match_boundaries, "Boundary_").
374
+ sub(match_date_header, "Date:\r\n").
375
+ sub(match_lib_version, " (Ruby MIME v0.0)")
376
+ end
377
+
378
+ #
379
+ # Return the absolute path of +file+ under the test/scaffold directory.
380
+ #
381
+ def sd file
382
+ @scaffold_dir ||= File.join(File.dirname(__FILE__), 'scaffold')
383
+ @scaffold_dir + file
384
+ end
385
+
386
+ end