mime 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -12,6 +12,7 @@ multipart/form-data transactions.
12
12
  * MIME::CompositeMediaType for a description of composite media types
13
13
  * MIME::DiscreteMediaType for a description of discrete media types
14
14
  * MIME::DiscreteMediaFactory for easy programming of discrete media types
15
+ * MIME::ContentFormats for ways to encode/decode discrete media types
15
16
 
16
17
 
17
18
  == Media Type Inheritance Heirarchy
@@ -92,6 +93,7 @@ bodies.
92
93
  <em>First things first!</em>
93
94
 
94
95
  require 'mime'
96
+ include MIME # allow ommision of "MIME::" namespace in examples below
95
97
 
96
98
 
97
99
  === Instantiate a DiscreteMediaType object
@@ -103,7 +105,7 @@ file and determine the MIME type for you.
103
105
 
104
106
  file = '/tmp/data.xml'
105
107
 
106
- text_media = TextMedia.new(File.read(file), 'text/xml')} # media class
108
+ text_media = TextMedia.new(File.read(file), 'xml')} # media class
107
109
  text_media = DiscreteMediaFactory.create(file) # media factory
108
110
 
109
111
  Discrete media objects can then be embedded in MIME messages as we will see in
@@ -121,14 +123,14 @@ SMTP client.
121
123
  msg.subject = 'This is important' # add subject
122
124
  msg.headers.add('Priority', 'urgent') # add custom header
123
125
 
124
- msg.body = TextMedia.new('hello, world!')
126
+ msg.body = TextMedia.new('hello, world!', 'plain', 'charset' => 'us-ascii')
125
127
  #
126
128
  # The following two snippets are equivalent to the previous line.
127
129
  #
128
130
  # msg.body = "\r\nhello, world!"
129
131
  # msg.header.add('Content-Type', 'text/plain; charset=us-ascii')
130
132
  #
131
- # --OR-- (notice the header must come first, followed by CRLFx2)
133
+ # --OR-- (notice the header must come first, followed by two CRLFs)
132
134
  #
133
135
  # msg.body = "Content-Type: text/plain; charset=us-ascii\r\n\r\nhello, world!"
134
136
 
@@ -164,13 +166,13 @@ The multipart/alternative content type allows for multiple, alternatively
164
166
  formatted versions of the same content, such as plain text and HTML. Clients are
165
167
  then responsible for choosing the most suitable version for display.
166
168
 
167
- text_msg = TextMedia.new(<<TEXT_DATA, 'text/plain')
169
+ text_msg = TextMedia.new(<<TEXT_DATA, 'plain')
168
170
  **Hello, world!**
169
171
 
170
172
  Ruby is cool!
171
173
  TEXT_DATA
172
174
 
173
- html_msg = TextMedia.new(<<HTML_DATA, 'text/html')
175
+ html_msg = TextMedia.new(<<HTML_DATA, 'html')
174
176
  <html>
175
177
  <body>
176
178
  <h1>Hello, world!</h1>
@@ -197,7 +199,7 @@ Notice the _img_ tag _src_.
197
199
  image = DiscreteMediaFactory.create('/tmp/ruby.png')
198
200
  image.content_transfer_encoding = 'binary'
199
201
 
200
- html_msg = TextMedia.new(<<EOF, 'text/html; charset=iso-8859-1')
202
+ html_msg = TextMedia.new(<<EOF, 'html', 'charset' => 'iso-8859-1')
201
203
  <html>
202
204
  <body>
203
205
  <h1>Ruby Image</h1>
@@ -233,7 +235,7 @@ HTTP server. It contains a single text input and a file input.
233
235
  portrait_filename = '/tmp/joe_portrait.jpg'
234
236
 
235
237
  portrait_field = open(portrait_filename) do |f|
236
- ImageMedia.new(f.read, 'image/jpeg') # explicit content type
238
+ ImageMedia.new(f.read, 'jpeg') # explicit content type
237
239
  end
238
240
  portrait_field.content_transfer_encoding = 'binary'
239
241
 
@@ -249,7 +251,7 @@ HTTP server. It contains a single text input and a file input.
249
251
  === HTML form with file upload via DiscreteMediaFactory
250
252
 
251
253
  The outcome of this example is identical to the previous one. The only semantic
252
- difference is that the DiscreteMediaFactory class is used to instantiate the
254
+ difference is that the DiscreteMediaFactory module is used to instantiate the
253
255
  image object.
254
256
 
255
257
  name_field = TextMedia.new('Joe Blow')
@@ -264,6 +266,28 @@ image object.
264
266
  form_data.to_s
265
267
 
266
268
 
269
+ === Avoid "embarrassing line wraps" using flowed format for text/plain
270
+
271
+ Text/Plain is usually displayed as preformatted text, often in a fixed font.
272
+ That is, the characters start at the left margin of the display window, and
273
+ advance to the right until a CRLF sequence is seen, at which point a new line
274
+ is started, again at the left margin. When a line length exceeds the display
275
+ window, some clients will wrap the line, while others invoke a horizontal
276
+ scroll bar. The result: embarrassing line wraps.
277
+
278
+ Flowed format allows the sender to express to the receiver which lines can be
279
+ considered a logical paragraph, and thus flowed (wrapped and joined) as
280
+ appropriate.
281
+
282
+ long_paragraph =
283
+ "This is a continuous fixed-line-length paragraph that is longer than " +
284
+ "80 characters and will be soft line wrapped after the word '80'.\n\n"
285
+
286
+ flowed_txt = ContentFormats::TextFlowed.encode(long_paragraph * 2)
287
+ flowed_msg = TextMedia.new(flowed_txt, 'plain', 'format' => 'flowed')
288
+ flowed_msg.to_s # neatly formatted text compatible with large to small screens
289
+
290
+
267
291
  == More Examples
268
292
 
269
293
  For many more examples, check the test class MIMETest.
@@ -276,13 +300,20 @@ Documentation :: http://ecentryx.com/gems/mime
276
300
 
277
301
  == History
278
302
 
279
- 1. 2008-11-05, v0.1: First public release
280
- 2. 2013-11-17, v0.2.0: Update for Ruby 1.9.3
303
+ 1. 2008-11-05, v0.1
304
+ * First public release.
305
+ 2. 2013-12-18, v0.2.0
306
+ * Update for Ruby 1.9.3.
281
307
  * Update Rakefile test, package, and rdoc tasks.
282
308
  * Change test suite from Test::Unit to Minitest.
283
309
  * Cleanup existing and add new tests cases.
284
310
  * Clarify code comments and README examples.
285
311
  * Fix content type detection.
312
+ 3. 2014-02-28, v0.3.0
313
+ * Simplify API of DiscreteMediaType subclasses.
314
+ * Disallow Content-Type changes after instantiating DiscreteMediaType.
315
+ * Add flowed format support for text/plain (RFC 2646).
316
+
286
317
 
287
318
  == License
288
319
 
data/lib/mime.rb CHANGED
@@ -18,7 +18,11 @@
18
18
  #
19
19
  module MIME
20
20
 
21
- VERSION = '0.2.0'
21
+ VERSION = '0.3.0'
22
+
23
+ module ContentFormats
24
+ MAX_SMTP_LINE = 997
25
+ end
22
26
 
23
27
  end
24
28
 
@@ -30,3 +34,4 @@ require 'mime/discrete_media_type'
30
34
  require 'mime/composite_media_type'
31
35
  require 'mime/message'
32
36
  require 'mime/discrete_media_factory'
37
+ require 'mime/content_formats/text_flowed'
@@ -0,0 +1,119 @@
1
+ module MIME::ContentFormats
2
+
3
+ #
4
+ # Minimal implementation of RFC 2646: The Text/Plain Format Parameter
5
+ #
6
+ # https://www.ietf.org/rfc/rfc2646.txt
7
+ #
8
+ # == Excerpts from RFC
9
+ #
10
+ # This memo proposes a new parameter to be used with Text/Plain, and, in the
11
+ # presence of this parameter, the use of trailing whitespace to indicate
12
+ # flowed lines. This results in an encoding which appears as normal
13
+ # Text/Plain in older implementations, since it is in fact normal
14
+ # Text/Plain.
15
+ #
16
+ # Each paragraph is displayed, starting at the left margin (or paragraph
17
+ # indent), and continuing to the right until a word is encountered which
18
+ # does not fit in the remaining display width. This word is displayed at the
19
+ # left margin of the next line. This continues until the paragraph ends (a
20
+ # CRLF is seen).
21
+ #
22
+ # == MIME format parameter to the text/plain media type
23
+ #
24
+ # Name: Format
25
+ # Value: Fixed, Flowed
26
+ # Example: Content-Type: text/plain; charset=iso-8859-1; format=flowed
27
+ #
28
+ #--
29
+ # == TODO
30
+ # - Implement RFC 3676, which obsoletes RFC 2646.
31
+ # - Usenet signature convention (section 4.3)
32
+ # - Space-Stuffing (section 4.4)
33
+ # - Quoting (section 4.5)
34
+ # - Perhaps this should be subsumed into the MIME project.
35
+ #
36
+ module TextFlowed
37
+
38
+ MAX_FLOWED_LINE = 79
39
+
40
+ #
41
+ # Encode plain +text+ into flowed format, reducing long lines to +max+
42
+ # characters or less using soft line breaks (i.e., SPACE+CRLF).
43
+ #
44
+ # The default +max+ line length is 79 characters. According to the RFC, 66
45
+ # and 72 characters is also common.
46
+ #
47
+ # The features of RFC 2646, such as line quoting and space-stuffing,
48
+ # are not implemented.
49
+ #
50
+ def self.encode(text, max = MAX_FLOWED_LINE)
51
+ if max > 79
52
+ raise ArgumentError, 'flowed lines must be 79 characters or less'
53
+ end
54
+
55
+ out = []
56
+ text.split(/\r\n|\n/).each do |paragraph|
57
+ # tab use is discouraged
58
+ # http://tools.ietf.org/html/rfc822#section-3.4.2
59
+ paragraph.gsub!(/\t/, ' '*4)
60
+
61
+ # trim spaces before hard break
62
+ # http://tools.ietf.org/html/rfc2646#section-4.1
63
+ paragraph.rstrip!
64
+
65
+ if paragraph.length <= max
66
+ out << paragraph
67
+ else # flow text
68
+ line = ''
69
+ word = ''
70
+
71
+ paragraph.each_char do |char|
72
+ if char == ' '
73
+ # Omit spaces after soft break to prevent stuffing on next line.
74
+ next if word.empty? && (line.size == 0 || line.size == max)
75
+
76
+ if (line.size + word.size) < max
77
+ line << word + char
78
+ else # soft break situation
79
+ unless line.empty?
80
+ out << line.dup
81
+ line.clear
82
+ end
83
+ if word.size < max
84
+ line << word + char
85
+ else
86
+ word.scan(/.{1,#{MAX_SMTP_LINE}}/) {|s| out << s }
87
+ end
88
+ end
89
+ word.clear
90
+ else # accumulate non-space characters in buffer
91
+ word += char
92
+ end
93
+ end
94
+
95
+ # flush buffers in an orderly fashion
96
+ if ! word.empty?
97
+ if (line.size + word.size) <= max
98
+ out << line + word
99
+ else
100
+ out << line unless line.empty?
101
+ word.scan(/.{1,#{MAX_SMTP_LINE}}/) {|s| out << s }
102
+ end
103
+ elsif ! line.empty?
104
+ out << line
105
+ end
106
+ end
107
+ end
108
+
109
+ out.join("\r\n")
110
+ end
111
+
112
+ #
113
+ # Decode flowed plain +text+.
114
+ #
115
+ def self.decode(text)
116
+ raise NotImplementedError
117
+ end
118
+ end
119
+ end
@@ -86,11 +86,12 @@ module MIME
86
86
  #
87
87
  def file_type file
88
88
  filename = file.respond_to?(:path) ? file.path : file
89
- extension = File.extname(filename)[1..-1] # array index removes period
90
- typ_exts = CONTENT_TYPES.find {|type, extentions| extentions.include? extension}
91
- typ_exts ? typ_exts.first : typ_exts # returns +type+ from above or nil
89
+ extension = File.extname(filename)[1..-1] # remove leading period
90
+ type, _ = CONTENT_TYPES.find do |content_type, extentions|
91
+ extentions.include? extension
92
+ end
93
+ type
92
94
  end
93
95
 
94
96
  end
95
-
96
97
  end
@@ -1,9 +1,9 @@
1
1
  module MIME
2
2
 
3
3
  #
4
- # Class object used only for initializing derived DiscreteMediaType objects.
4
+ # Module used only for initializing derived DiscreteMediaType objects.
5
5
  #
6
- class DiscreteMediaFactory
6
+ module DiscreteMediaFactory
7
7
 
8
8
  class << self
9
9
 
@@ -42,13 +42,18 @@ module MIME
42
42
  fname = file
43
43
  end
44
44
 
45
+ type, subtype = ctype.to_s.split('/')
46
+ if type.to_s.empty? || subtype.to_s.empty?
47
+ raise UnknownContentError, "invalid content type: #{ctype}"
48
+ end
49
+
45
50
  media_obj =
46
- case ctype.to_s[/^(\w+)\//, 1]
47
- when 'application'; ApplicationMedia.new(cntnt, ctype)
48
- when 'audio' ; AudioMedia.new(cntnt, ctype)
49
- when 'image' ; ImageMedia.new(cntnt, ctype)
50
- when 'text' ; TextMedia.new(cntnt, ctype)
51
- when 'video' ; VideoMedia.new(cntnt, ctype)
51
+ case type
52
+ when 'application'; ApplicationMedia.new(cntnt, subtype)
53
+ when 'audio' ; AudioMedia.new(cntnt, subtype)
54
+ when 'image' ; ImageMedia.new(cntnt, subtype)
55
+ when 'text' ; TextMedia.new(cntnt, subtype)
56
+ when 'video' ; VideoMedia.new(cntnt, subtype)
52
57
  else raise UnknownContentError, "invalid content type: #{ctype}"
53
58
  end
54
59
 
@@ -56,13 +61,6 @@ module MIME
56
61
  media_obj.path = fname
57
62
  media_obj
58
63
  end
59
-
60
64
  end
61
-
62
- def initialize
63
- AbstractClassError.no_instantiation self, DiscreteMediaFactory
64
- end
65
-
66
65
  end
67
-
68
66
  end
@@ -8,12 +8,10 @@ module MIME
8
8
  # This class is abstract.
9
9
  #
10
10
  class DiscreteMediaType < MediaType
11
-
12
- def initialize body, content_type = 'application/octet-stream'
11
+ def initialize(body, media_subtype, content_params)
13
12
  AbstractClassError.no_instantiation(self, DiscreteMediaType)
14
- super
13
+ super(body, "#{@media_type}/#{media_subtype}", content_params)
15
14
  end
16
-
17
15
  end
18
16
 
19
17
  #
@@ -24,56 +22,57 @@ module MIME
24
22
  # ApplicationMedia is the catch all class. If your content cannot be
25
23
  # identified as another DiscreteMediaType, then it is application media.
26
24
  #
27
- # See DiscreteMediaType.new for initialization parameters.
28
- #
29
25
  class ApplicationMedia < DiscreteMediaType
26
+ def initialize(body, subtype = 'octet-stream', params = {})
27
+ @media_type = 'application'
28
+ super
29
+ end
30
30
  end
31
31
 
32
32
  #
33
- # AudioMedia is intended for discrete audio content. The content subtype
34
- # indicates the specific audio format.
35
- #
36
- # See DiscreteMediaType.new for initialization parameters.
33
+ # AudioMedia is intended for discrete audio content. The +subtype+ indicates
34
+ # the specific audio format, such as *mpeg* or *midi*.
37
35
  #
38
36
  class AudioMedia < DiscreteMediaType
37
+ def initialize(body, subtype = 'basic', params = {})
38
+ @media_type = 'audio'
39
+ super
40
+ end
39
41
  end
40
42
 
41
43
  #
42
- # ImageMedia is intented for discrete image content. The content subtype
43
- # indicates the specific image format.
44
- #
45
- # See DiscreteMediaType.new for initialization parameters.
44
+ # ImageMedia is intented for discrete image content. The +subtype+ indicates
45
+ # the specific image format, such as *jpeg* or *gif*.
46
46
  #
47
47
  class ImageMedia < DiscreteMediaType
48
+ def initialize(body, subtype = 'jpeg', params = {})
49
+ @media_type = 'image'
50
+ super
51
+ end
48
52
  end
49
53
 
50
54
  #
51
- # TextMedia is intended for content which is principally textual in form.
55
+ # TextMedia is intended for content which is principally textual in form. The
56
+ # +subtype+ indicates the specific text type, such as *plain* or *html*.
52
57
  #
53
58
  class TextMedia < DiscreteMediaType
54
-
55
- #
56
- # Return a new TextMedia object containing +body+ with the content type of
57
- # +content_type+.
58
- #
59
- # To specify the character set of +body+, a _charset_ parameter may be
60
- # appended to +content_type+ using a semi-colon delimiter.
61
- #
62
- def initialize body, content_type = 'text/plain; charset=us-ascii'
59
+ def initialize(body, subtype = 'plain', params = {})
60
+ @media_type = 'text'
63
61
  super
64
62
  end
65
-
66
63
  end
67
64
 
68
65
  #
69
- # VideoMedia is intended for discrete video content. The content subtype
66
+ # VideoMedia is intended for discrete video content. The content +subtype+
70
67
  # indicates the specific video format. The RFC describes video media as
71
68
  # content that contains a time-varying-picture image, possibly with color and
72
69
  # coordinated sound.
73
70
  #
74
- # See DiscreteMediaType.new for initialization parameters.
75
- #
76
71
  class VideoMedia < DiscreteMediaType
72
+ def initialize(body, subtype = 'mpeg', params = {})
73
+ @media_type = 'video'
74
+ super
75
+ end
77
76
  end
78
77
 
79
78
  end
@@ -17,9 +17,7 @@ module MIME
17
17
  # RFC 2822 CRLF line separator.
18
18
  #
19
19
  def to_s
20
- @headers.collect do |name, value|
21
- "#{name}: #{value}"
22
- end.join("\r\n")
20
+ @headers.to_a.map {|kv| kv.join(": ")}.join("\r\n")
23
21
  end
24
22
 
25
23
  #
@@ -59,6 +59,16 @@ module MIME
59
59
  headers.add('Content-Transfer-Encoding', encoding)
60
60
  end
61
61
 
62
+ #
63
+ # Currently only version 1.0 exists.
64
+ #
65
+ def mime_version= version
66
+ @mime_version = version
67
+ headers.add('MIME-Version', version)
68
+ end
69
+
70
+ protected
71
+
62
72
  #
63
73
  # Specifies the media type and subtype of the content. +type+ will have
64
74
  # the form <em>media-type/subtype</em>.
@@ -75,16 +85,6 @@ module MIME
75
85
  headers.add('Content-Type', type)
76
86
  end
77
87
 
78
- #
79
- # Currently only version 1.0 exists.
80
- #
81
- def mime_version= version
82
- @mime_version = version
83
- headers.add('MIME-Version', version)
84
- end
85
-
86
- protected
87
-
88
88
  #
89
89
  # +type+ is the disposition type of either "inline" or "attachment".
90
90
  # +params+ is a Hash with zero or more of the following keys:
@@ -15,13 +15,19 @@ module MIME
15
15
  attr_accessor :body
16
16
  protected :body, :body=
17
17
 
18
- def initialize body, content_type
18
+ def initialize body, content_type, content_params = {}
19
19
  AbstractClassError.no_instantiation(self, MediaType)
20
20
 
21
21
  @headers = HeaderContainer.new
22
22
  @body = body
23
23
  self.content_id = unique_id
24
- self.content_type = content_type
24
+ self.content_type =
25
+ if content_params.empty?
26
+ content_type
27
+ else
28
+ params = content_params.to_a.map {|kv| kv.join('=')}.join('; ')
29
+ "#{content_type}; #{params}"
30
+ end
25
31
  end
26
32
 
27
33
  #
@@ -38,7 +38,7 @@ Content-Disposition: form-data; name="htm"
38
38
 
39
39
  --Boundary_88135231713600.3996846921776409
40
40
  Content-ID: <88135231725400.11623307734175536>
41
- Content-Type: text/plain; charset=us-ascii
41
+ Content-Type: text/plain
42
42
  Content-Disposition: form-data; name="txt"
43
43
 
44
44
  text body
@@ -6,6 +6,6 @@ Cc: boss@example.com <Head Honcho>
6
6
  From: jane@example.com
7
7
  Subject: This is an important email
8
8
  Content-ID: <102975158103800.7998663631497803>
9
- Content-Type: text/plain; charset=us-ascii
9
+ Content-Type: text/plain
10
10
 
11
11
  Hello, world!
@@ -2,6 +2,6 @@ Date: Wed, 29 Oct 2008 14:55:34 -0700
2
2
  Message-ID: <357114433@1021851988>
3
3
  MIME-Version: 1.0 (Ruby MIME v0.1.0)
4
4
  Content-ID: <10218519780.0838656876757202>
5
- Content-Type: text/plain; charset=us-ascii
5
+ Content-Type: text/plain
6
6
 
7
7
  a plain text message
data/test/test_mime.rb CHANGED
@@ -26,7 +26,7 @@ class MIMETest < Minitest::Test
26
26
  end
27
27
 
28
28
  def test_make_audio_message
29
- audio_media = MIME::AudioMedia.new(BINARY_DATA, 'audio/midi')
29
+ audio_media = MIME::AudioMedia.new(BINARY_DATA, 'midi')
30
30
  audio_media.content_transfer_encoding = 'binary'
31
31
  assert_equal_mime_msg 'audio.msg', MIME::Message.new(audio_media)
32
32
  end
@@ -40,7 +40,6 @@ class MIMETest < Minitest::Test
40
40
  def test_make_image_message
41
41
  image = IO.read(sd('image.jpg'))
42
42
  image_media = MIME::ImageMedia.new(image)
43
- image_media.content_type = 'image/jpeg'
44
43
  image_media.content_transfer_encoding = 'binary'
45
44
  assert_equal_mime_msg 'image.msg', MIME::Message.new(image_media)
46
45
  end
@@ -52,7 +51,6 @@ class MIMETest < Minitest::Test
52
51
 
53
52
  def test_make_video_message
54
53
  video_media = MIME::VideoMedia.new(BINARY_DATA)
55
- video_media.content_type = 'video/mpeg'
56
54
  video_media.content_transfer_encoding = 'binary'
57
55
  assert_equal_mime_msg 'video.msg', MIME::Message.new(video_media)
58
56
  end
@@ -80,8 +78,8 @@ class MIMETest < Minitest::Test
80
78
  xml_data = IO.read(sd('data.xml'))
81
79
 
82
80
  txt = MIME::TextMedia.new(txt_data)
83
- htm = MIME::TextMedia.new(htm_data, 'text/html')
84
- xml = MIME::TextMedia.new(xml_data, 'text/xml')
81
+ htm = MIME::TextMedia.new(htm_data, 'html')
82
+ xml = MIME::TextMedia.new(xml_data, 'xml')
85
83
 
86
84
  form = MIME::MultipartMedia::FormData.new
87
85
  form.add_entity txt, 'txt'
@@ -95,13 +93,13 @@ class MIMETest < Minitest::Test
95
93
  img2_filename = 'ruby.png'
96
94
  img1_data = IO.read(sd(img1_filename))
97
95
  img2_data = IO.read(sd(img2_filename))
98
- img1 = MIME::ImageMedia.new(img1_data, 'image/jpeg')
99
- img2 = MIME::ImageMedia.new(img2_data, 'image/png')
96
+ img1 = MIME::ImageMedia.new(img1_data, 'jpeg')
97
+ img2 = MIME::ImageMedia.new(img2_data, 'png')
100
98
  img1.content_transfer_encoding = '8bit'
101
99
  img2.content_transfer_encoding = '8bit'
102
100
 
103
101
  desc_data = 'This is plain text description of images.'
104
- desc = MIME::TextMedia.new(desc_data)
102
+ desc = MIME::TextMedia.new(desc_data, 'plain', 'charset' => 'us-ascii')
105
103
 
106
104
  form = MIME::MultipartMedia::FormData.new
107
105
  form.add_entity desc, 'description'
@@ -130,11 +128,8 @@ class MIMETest < Minitest::Test
130
128
  def test_multipart_alternative_message
131
129
  txt_data = "*Header*\nmessage"
132
130
  htm_data = "<html><body><h1>Header</h1><p>message</p></body></html>"
133
- txt_msg = MIME::TextMedia.new(txt_data)
134
- htm_msg = MIME::TextMedia.new(htm_data)
135
-
136
- txt_msg.content_type = ('text/enhanced; charset=us-ascii')
137
- htm_msg.content_type = ('text/html; charset=iso-8859-1')
131
+ txt_msg = MIME::TextMedia.new(txt_data, 'enhanced', 'charset' => 'us-ascii')
132
+ htm_msg = MIME::TextMedia.new(htm_data, 'html', 'charset' => 'iso-8859-1')
138
133
 
139
134
  msg = MIME::MultipartMedia::Alternative.new
140
135
  msg.add_entity htm_msg
@@ -146,7 +141,7 @@ class MIMETest < Minitest::Test
146
141
  img = MIME::DiscreteMediaFactory.create(sd('ruby.png'))
147
142
  img.content_transfer_encoding = 'binary'
148
143
 
149
- html_msg = MIME::TextMedia.new(<<EOF, 'text/html; charset=iso-8859-1')
144
+ html_msg = MIME::TextMedia.new(<<EOF, 'html', 'charset' => 'iso-8859-1')
150
145
  <html>
151
146
  <body>
152
147
  <h1>HTML multipart/alternative message</h1>
@@ -158,7 +153,7 @@ class MIMETest < Minitest::Test
158
153
  EOF
159
154
  html_msg.content_transfer_encoding = '7bit'
160
155
 
161
- text_msg = MIME::TextMedia.new(<<EOF)
156
+ text_msg = MIME::TextMedia.new(<<EOF, 'plain', 'charset' => 'us-ascii')
162
157
  *HTML multipart/alternative message*
163
158
  txt before pix
164
159
  <cool ruby image>
@@ -181,7 +176,7 @@ EOF
181
176
 
182
177
  open(sd('image.jpg')) do |img_file|
183
178
  img_data = img_file.read
184
- img_msg = MIME::ImageMedia.new(img_data, 'image/jpeg')
179
+ img_msg = MIME::ImageMedia.new(img_data, 'jpeg')
185
180
  msg.attach_entity(img_msg, 'filename' => img_file.path)
186
181
  end
187
182
 
@@ -205,7 +200,7 @@ EOF
205
200
  img = MIME::DiscreteMediaFactory.create(sd('/ruby.png'))
206
201
  img.content_transfer_encoding = 'binary'
207
202
 
208
- html_msg = MIME::TextMedia.new(<<EOF, 'text/html; charset=iso-8859-1')
203
+ html_msg = MIME::TextMedia.new(<<EOF, 'html; charset=iso-8859-1')
209
204
  <html>
210
205
  <body>
211
206
  <h1>HTML multipart/related message</h1>
@@ -294,8 +289,8 @@ EOF
294
289
 
295
290
  def test_no_instantiation_of_abstract_classes
296
291
  e = MIME::AbstractClassError
297
- assert_raises(e) {MIME::MediaType.new(nil, nil)}
298
- assert_raises(e) {MIME::DiscreteMediaType.new(nil)}
292
+ assert_raises(e) {MIME::MediaType.new(nil, nil, nil)}
293
+ assert_raises(e) {MIME::DiscreteMediaType.new(nil, nil, nil)}
299
294
  assert_raises(e) {MIME::CompositeMediaType.new(nil)}
300
295
  assert_raises(e) {MIME::MultipartMedia.new(nil)}
301
296
  end
@@ -352,6 +347,7 @@ EOF
352
347
  def test_discrete_media_factory_with_specified_invalid_conent_type
353
348
  invalid_ctype1 = 'application-x/pdf'
354
349
  invalid_ctype2 = 'application'
350
+ invalid_ctype3 = ''
355
351
  valid_ctype = 'application/pdf'
356
352
  pdf = sd('book.pdf')
357
353
 
@@ -361,6 +357,9 @@ EOF
361
357
  assert_raises(MIME::UnknownContentError) {
362
358
  MIME::DiscreteMediaFactory.create(pdf, invalid_ctype2)
363
359
  }
360
+ assert_raises(MIME::UnknownContentError) {
361
+ MIME::DiscreteMediaFactory.create(pdf, invalid_ctype3)
362
+ }
364
363
  assert MIME::DiscreteMediaFactory.create(pdf, valid_ctype)
365
364
  end
366
365
 
@@ -0,0 +1,130 @@
1
+ gem 'minitest' # minitest in 1.9 stdlib is crufty
2
+ require 'minitest/autorun'
3
+ require 'mime'
4
+
5
+ class TextFlowedTest < Minitest::Test
6
+
7
+ include MIME::ContentFormats
8
+
9
+ def test_single_word_will_not_break_regardless_of_line_length
10
+ txt = "1234567890"
11
+ assert_equal txt, TextFlowed.encode(txt, 9) # <
12
+ assert_equal txt, TextFlowed.encode(txt, 10) # =
13
+ assert_equal txt, TextFlowed.encode(txt, 11) # >
14
+ end
15
+
16
+ def test_max_line_length_around_first_word
17
+ txt = "1234567890 A"
18
+ expected1 = "1234567890\r\nA"
19
+ expected2 = "1234567890 \r\nA"
20
+ assert_equal expected1, TextFlowed.encode(txt, 9) # <
21
+ assert_equal expected1, TextFlowed.encode(txt, 10) # =
22
+ assert_equal expected2, TextFlowed.encode(txt, 11) # >
23
+ end
24
+
25
+ def test_words_with_variable_spacing
26
+ txt = "123 456 789"
27
+ expected1 = "123 \r\n456 \r\n789"
28
+ expected2 = "123 456 \r\n789"
29
+ expected3 = "123 456 \r\n789"
30
+ expected4 = "123 456 \r\n789"
31
+ expected5 = txt
32
+ assert_equal expected1, TextFlowed.encode(txt, 8)
33
+ assert_equal expected2, TextFlowed.encode(txt, 9)
34
+ assert_equal expected3, TextFlowed.encode(txt, 10)
35
+ assert_equal expected4, TextFlowed.encode(txt, 11)
36
+ assert_equal expected4, TextFlowed.encode(txt, 12)
37
+ assert_equal expected4, TextFlowed.encode(txt, 13)
38
+ assert_equal expected5, TextFlowed.encode(txt, 14)
39
+ assert_equal expected5, TextFlowed.encode(txt, 15)
40
+ end
41
+
42
+ def test_paragraphs_with_variable_lengths
43
+ txt = "123 456 789\n\none two three\n\nEND"
44
+ expected1 = "123 \r\n456 \r\n789\r\n\r\none \r\ntwo \r\nthree\r\n\r\nEND"
45
+ expected2 = "123 456 \r\n789\r\n\r\none two \r\nthree\r\n\r\nEND"
46
+ expected3 = "123 456 789\r\n\r\none two \r\nthree\r\n\r\nEND"
47
+ expected4 = "123 456 789\r\n\r\none two three\r\n\r\nEND"
48
+ assert_equal expected1, TextFlowed.encode(txt, 4)
49
+ assert_equal expected2, TextFlowed.encode(txt, 10)
50
+ assert_equal expected3, TextFlowed.encode(txt, 11)
51
+ assert_equal expected3, TextFlowed.encode(txt, 12)
52
+ assert_equal expected4, TextFlowed.encode(txt, 13)
53
+ assert_equal expected4, TextFlowed.encode(txt, 14)
54
+ assert_equal expected4, TextFlowed.encode(txt, txt.length)
55
+ assert_equal expected4, TextFlowed.encode(txt, txt.length+1)
56
+ end
57
+
58
+ def test_trailing_newlines_removed
59
+ txt = "123 456 789\r\nabc def ghi"
60
+ assert_equal txt, TextFlowed.encode(txt+"\n", 11)
61
+ assert_equal txt, TextFlowed.encode(txt+"\r\n", 11)
62
+ end
63
+
64
+ def test_tab_expansion
65
+ txt = "123\t456"
66
+ expected1 = "123\r\n456"
67
+ expected2 = "123 \r\n456"
68
+ expected3 = "123 \r\n456"
69
+ expected4 = "123 \r\n456"
70
+ expected5 = "123 \r\n456"
71
+ expected6 = "123 456"
72
+ assert_equal expected1, TextFlowed.encode(txt, 2)
73
+ assert_equal expected1, TextFlowed.encode(txt, 3)
74
+ assert_equal expected2, TextFlowed.encode(txt, 4)
75
+ assert_equal expected3, TextFlowed.encode(txt, 5)
76
+ assert_equal expected4, TextFlowed.encode(txt, 6)
77
+ assert_equal expected5, TextFlowed.encode(txt, 7)
78
+ assert_equal expected5, TextFlowed.encode(txt, 8)
79
+ assert_equal expected5, TextFlowed.encode(txt, 9)
80
+ assert_equal expected6, TextFlowed.encode(txt, 10)
81
+ end
82
+
83
+ def test_spacing_after_hard_line_break
84
+ txt = "123\r\n\r\n abc"
85
+ exp = "123\r\n\r\nabc"
86
+ assert_equal exp, TextFlowed.encode(txt, 4) # remove SP if line > max
87
+ assert_equal txt, TextFlowed.encode(txt, 5) # preserve SP if line == max
88
+ assert_equal txt, TextFlowed.encode(txt, 6) # preserve SP if line < max
89
+ end
90
+
91
+ def test_trim_spaces_before_hard_line_breaks
92
+ txt1 = "123 \r\n\r\n456"
93
+ txt2 = "123 \r\n\r\n456 "
94
+ expected = "123\r\n\r\n456"
95
+ assert_equal expected, TextFlowed.encode(txt1, 3)
96
+ assert_equal expected, TextFlowed.encode(txt1, 4)
97
+ assert_equal expected, TextFlowed.encode(txt1, 5)
98
+ assert_equal expected, TextFlowed.encode(txt2, 3)
99
+ assert_equal expected, TextFlowed.encode(txt2, 4)
100
+ assert_equal expected, TextFlowed.encode(txt2, 5)
101
+ end
102
+
103
+ def test_trim_trailing_spaces
104
+ txt = "123 456 "
105
+ expected = "123 \r\n456"
106
+ assert_equal expected, TextFlowed.encode(txt, 4)
107
+ end
108
+
109
+ def test_rfc_79_character_max_flowed_line_length
110
+ TextFlowed.encode('123', 79) # no error
111
+ assert_raises(ArgumentError) { TextFlowed.encode('123', 80) }
112
+ end
113
+
114
+ def test_rfc_max_smtp_line_length_boundary
115
+ long_word = 'x' * MIME::ContentFormats::MAX_SMTP_LINE
116
+ txt1 = long_word
117
+ txt2 = 'x' + long_word
118
+ txt3 = 'x ' + long_word
119
+ txt4 = 'x ' + long_word + ' x'
120
+ expected1 = long_word
121
+ expected2 = long_word + "\r\nx"
122
+ expected3 = "x \r\n" + long_word
123
+ expected4 = "x \r\n" + long_word + "\r\nx"
124
+ assert_equal expected1, TextFlowed.encode(txt1)
125
+ assert_equal expected2, TextFlowed.encode(txt2)
126
+ assert_equal expected3, TextFlowed.encode(txt3)
127
+ assert_equal expected4, TextFlowed.encode(txt4)
128
+ end
129
+
130
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-12-18 00:00:00.000000000 Z
12
+ date: 2014-02-28 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: ! 'A library for building RFC compliant Multipurpose Internet Mail Extensions
15
15
 
@@ -39,6 +39,7 @@ files:
39
39
  - lib/mime/composite_media_type.rb
40
40
  - lib/mime/discrete_media_type.rb
41
41
  - lib/mime/message.rb
42
+ - lib/mime/content_formats/text_flowed.rb
42
43
  - lib/mime.rb
43
44
  - test/scaffold/application.msg
44
45
  - test/scaffold/unknown.yyy
@@ -65,6 +66,7 @@ files:
65
66
  - test/scaffold/data.htm
66
67
  - test/test_mime.rb-try
67
68
  - test/test_mime.rb
69
+ - test/test_text_flowed.rb
68
70
  homepage: http://ecentryx.com/gems/mime
69
71
  licenses:
70
72
  - ISC
@@ -92,3 +94,4 @@ specification_version: 3
92
94
  summary: Multipurpose Internet Mail Extensions (MIME) Library
93
95
  test_files:
94
96
  - test/test_mime.rb
97
+ - test/test_text_flowed.rb