mime 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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