mime 0.2.0 → 0.3.0
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.
- data/README.rdoc +41 -10
- data/lib/mime.rb +6 -1
- data/lib/mime/content_formats/text_flowed.rb +119 -0
- data/lib/mime/content_types.rb +5 -4
- data/lib/mime/discrete_media_factory.rb +13 -15
- data/lib/mime/discrete_media_type.rb +27 -28
- data/lib/mime/header_container.rb +1 -3
- data/lib/mime/headers/mime.rb +10 -10
- data/lib/mime/media_type.rb +8 -2
- data/test/scaffold/multipart_form_data_mixed.msg +0 -0
- data/test/scaffold/multipart_form_data_text.msg +1 -1
- data/test/scaffold/multipart_mixed_inline_and_attachment.msg +0 -0
- data/test/scaffold/plain_text_email.msg +1 -1
- data/test/scaffold/text.msg +1 -1
- data/test/test_mime.rb +18 -19
- data/test/test_text_flowed.rb +130 -0
- metadata +5 -2
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), '
|
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
|
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, '
|
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, '
|
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, '
|
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, '
|
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
|
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
|
280
|
-
|
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.
|
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
|
data/lib/mime/content_types.rb
CHANGED
@@ -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] #
|
90
|
-
|
91
|
-
|
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
|
-
#
|
4
|
+
# Module used only for initializing derived DiscreteMediaType objects.
|
5
5
|
#
|
6
|
-
|
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
|
47
|
-
when 'application'; ApplicationMedia.new(cntnt,
|
48
|
-
when 'audio' ; AudioMedia.new(cntnt,
|
49
|
-
when 'image' ; ImageMedia.new(cntnt,
|
50
|
-
when 'text' ; TextMedia.new(cntnt,
|
51
|
-
when 'video' ; VideoMedia.new(cntnt,
|
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
|
34
|
-
#
|
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
|
43
|
-
#
|
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
|
data/lib/mime/headers/mime.rb
CHANGED
@@ -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:
|
data/lib/mime/media_type.rb
CHANGED
@@ -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 =
|
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
|
#
|
Binary file
|
@@ -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
|
41
|
+
Content-Type: text/plain
|
42
42
|
Content-Disposition: form-data; name="txt"
|
43
43
|
|
44
44
|
text body
|
Binary file
|
data/test/scaffold/text.msg
CHANGED
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, '
|
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, '
|
84
|
-
xml = MIME::TextMedia.new(xml_data, '
|
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, '
|
99
|
-
img2 = MIME::ImageMedia.new(img2_data, '
|
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, '
|
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, '
|
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, '
|
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.
|
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:
|
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
|