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.
- data/README +256 -0
- data/Rakefile +34 -0
- data/lib/mime.rb +32 -0
- data/lib/mime/composite_media_type.rb +169 -0
- data/lib/mime/content_types.rb +96 -0
- data/lib/mime/discrete_media_factory.rb +69 -0
- data/lib/mime/discrete_media_type.rb +79 -0
- data/lib/mime/error.rb +32 -0
- data/lib/mime/header_container.rb +34 -0
- data/lib/mime/headers/internet.rb +90 -0
- data/lib/mime/headers/mime.rb +118 -0
- data/lib/mime/media_type.rb +45 -0
- data/lib/mime/message.rb +51 -0
- data/lib/mime/parser.rb +16 -0
- data/test/mime_test.rb +386 -0
- data/test/scaffold/application.msg +8 -0
- data/test/scaffold/audio.msg +8 -0
- data/test/scaffold/book.pdf +0 -0
- data/test/scaffold/data.xml +17 -0
- data/test/scaffold/image.jpg +0 -0
- data/test/scaffold/image.msg +0 -0
- data/test/scaffold/index.html +6 -0
- data/test/scaffold/main.css +0 -0
- data/test/scaffold/mini.mov +0 -0
- data/test/scaffold/multipart_alternative.msg +17 -0
- data/test/scaffold/multipart_alternative_related.msg +0 -0
- data/test/scaffold/multipart_form_data_file.msg +0 -0
- data/test/scaffold/multipart_form_data_file_and_text.msg +0 -0
- data/test/scaffold/multipart_form_data_mixed.msg +0 -0
- data/test/scaffold/multipart_form_data_text.msg +40 -0
- data/test/scaffold/multipart_mixed_inline_and_attachment.msg +0 -0
- data/test/scaffold/multipart_mixed_inline_and_attachment2.msg +0 -0
- data/test/scaffold/multipart_related.msg +0 -0
- data/test/scaffold/plain_text_email.msg +9 -0
- data/test/scaffold/ruby.png +0 -0
- data/test/scaffold/song.mp3 +0 -0
- data/test/scaffold/text.msg +7 -0
- data/test/scaffold/unknown.yyy +1 -0
- data/test/scaffold/video.msg +8 -0
- metadata +92 -0
data/README
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
== Multipurpose Internet Mail Extensions (MIME)
|
2
|
+
|
3
|
+
A library for building RFC compliant Multipurpose Internet Mail Extensions
|
4
|
+
(MIME) messages. It can be used to construct standardized MIME messages for use
|
5
|
+
in client/server communications, such as Internet mail or HTTP
|
6
|
+
multipart/form-data transactions.
|
7
|
+
|
8
|
+
|
9
|
+
== See
|
10
|
+
|
11
|
+
* MIME for RFCs used to implement the library (other RFCs scattered throughout)
|
12
|
+
* MIME::CompositeMediaType for a description of composite media types
|
13
|
+
* MIME::DiscreteMediaType for a description of discrete media types
|
14
|
+
* MIME::DiscreteMediaFactory for easy programming of discrete media types
|
15
|
+
|
16
|
+
|
17
|
+
== Media Type Inheritance Heirarchy
|
18
|
+
|
19
|
+
MediaType*
|
20
|
+
^
|
21
|
+
|
|
22
|
+
|--DiscreteMediaType*
|
23
|
+
| ^
|
24
|
+
| |
|
25
|
+
| |--ApplicationMedia
|
26
|
+
| |--AudioMedia
|
27
|
+
| |--ImageMedia
|
28
|
+
| |--TextMedia
|
29
|
+
| +--VideoMedia
|
30
|
+
|
|
31
|
+
+--CompositeMediaType*
|
32
|
+
^
|
33
|
+
|
|
34
|
+
|--MessageMedia**
|
35
|
+
| ^
|
36
|
+
| |
|
37
|
+
| |--ExternalBody**
|
38
|
+
| |--Partial**
|
39
|
+
| +--RFC822**
|
40
|
+
|
|
41
|
+
+--MultipartMedia*
|
42
|
+
^
|
43
|
+
|
|
44
|
+
|--Alternative
|
45
|
+
|--Digest**
|
46
|
+
|--Encrypted**
|
47
|
+
|--FormData
|
48
|
+
|--Mixed
|
49
|
+
|--Parallel**
|
50
|
+
|--Related
|
51
|
+
|--Report**
|
52
|
+
+--Signed**
|
53
|
+
|
54
|
+
* Abstract Class
|
55
|
+
** Not implemented
|
56
|
+
|
57
|
+
|
58
|
+
== MIME Message Structure
|
59
|
+
|
60
|
+
|
61
|
+
---------------------+
|
62
|
+
+----------------+ |
|
63
|
+
| RFC822 & MIME | |
|
64
|
+
| Message Headers| |
|
65
|
+
+----------------+ |
|
66
|
+
---+ |
|
67
|
+
+----------------+ | |
|
68
|
+
| MIME Headers | | |
|
69
|
+
+----------------+ |---MIME Entity |
|
70
|
+
+----------------+ | (N) |
|
71
|
+
| Body | | |---RFC822 Message
|
72
|
+
+----------------+ | |
|
73
|
+
---+ |
|
74
|
+
---+ |
|
75
|
+
+----------------+ | |
|
76
|
+
| MIME Headers | | |
|
77
|
+
+----------------+ |---MIME Entity |
|
78
|
+
+----------------+ | (N+1) |
|
79
|
+
| Body | | |
|
80
|
+
+----------------+ | |
|
81
|
+
---+ |
|
82
|
+
---------------------+
|
83
|
+
|
84
|
+
Each <em>MIME Entity</em> must be a discrete (MIME::DiscreteMediaType) or
|
85
|
+
composite (MIME::CompositeMediaType) media type. Because MIME is recursive,
|
86
|
+
composite entity bodies may contain other composite or discrete entites and so
|
87
|
+
on. However, discrete entities are non-recursive and contain only non-MIME
|
88
|
+
bodies.
|
89
|
+
|
90
|
+
|
91
|
+
== Examples
|
92
|
+
|
93
|
+
<em>The following examples imply that the MIME module is included.</em>
|
94
|
+
|
95
|
+
|
96
|
+
=== Two ways to instantiate a DiscreteMediaType object using a file path
|
97
|
+
|
98
|
+
fpath = '/tmp/data.xml'
|
99
|
+
text_media = open(fpath) {|f| TextMedia.new(f.read, 'text/xml')}
|
100
|
+
text_media = DiscreteMediaFactory.create(fpath)
|
101
|
+
|
102
|
+
|
103
|
+
=== Simple text/plain RFC822 email
|
104
|
+
|
105
|
+
msg = Message.new # creates a blank message with date and message ID headers
|
106
|
+
msg.date = (Time.now - 3600).rfc2822 # specify a different date
|
107
|
+
msg.subject = 'This is important'
|
108
|
+
msg.headers.add('X-Priority', 'high') # custom header
|
109
|
+
|
110
|
+
msg.body = TextMedia.new('hello, it is me!')
|
111
|
+
#
|
112
|
+
# The following snippets are equivalent to the previous line.
|
113
|
+
#
|
114
|
+
# msg.body = "\r\nhello, it is me!"
|
115
|
+
# msg.header.add('Content-Type', 'text/plain; charset=us-ascii')
|
116
|
+
#
|
117
|
+
# --OR--
|
118
|
+
#
|
119
|
+
# msg.body = "Content-Type: text/plain; charset=us-ascii\r\n\r\nhello, it is me!"
|
120
|
+
|
121
|
+
msg.to = {
|
122
|
+
's13xj@x.com' => nil, # no name display
|
123
|
+
'james@x.com' => 'James',
|
124
|
+
'clint@x.com' => 'Clint',
|
125
|
+
}
|
126
|
+
msg.from = {
|
127
|
+
'theboss@x.com' => 'Boss Man'
|
128
|
+
}
|
129
|
+
|
130
|
+
msg.to_s # ready to be sent via SMTP
|
131
|
+
|
132
|
+
|
133
|
+
=== Plain text multipart/mixed message with a file attachment
|
134
|
+
|
135
|
+
The multipart/mixed content type can be used to aggregate multiple unrelated
|
136
|
+
entities.
|
137
|
+
|
138
|
+
text = DiscreteMediaFactory.create('/tmp/data.txt')
|
139
|
+
image = DiscreteMediaFactory.create('/tmp/ruby.png')
|
140
|
+
|
141
|
+
mixed_msg = MultipartMedia::Mixed.new
|
142
|
+
mixed_msg.attach_entity(image)
|
143
|
+
mixed_msg.add_entity(text)
|
144
|
+
mixed_msg.to_s
|
145
|
+
|
146
|
+
|
147
|
+
=== Plain text and HTML multipart/alternative MIME message
|
148
|
+
|
149
|
+
The multipart/alternative content type allows for multiple alternatively
|
150
|
+
formatted versions of the same content. Clients are then responsible for
|
151
|
+
choosing the most suitable version for display.
|
152
|
+
|
153
|
+
text_msg = TextMedia.new(<<-text_data, 'text/plain')
|
154
|
+
*Headline*
|
155
|
+
Ruby is cool!
|
156
|
+
text_data
|
157
|
+
|
158
|
+
html_msg = TextMedia.new(<<-html_data, 'text/html')
|
159
|
+
<html>
|
160
|
+
<body>
|
161
|
+
<h1>Headline</h1>
|
162
|
+
<p>Ruby is cool!</p>
|
163
|
+
</body>
|
164
|
+
</html>
|
165
|
+
html_data
|
166
|
+
|
167
|
+
msg = MultipartMedia::Alternative.new
|
168
|
+
msg.add_entity(html_msg) # most complex representation must be added first
|
169
|
+
msg.add_entity(text_msg)
|
170
|
+
msg.to_s
|
171
|
+
|
172
|
+
|
173
|
+
=== HTML multipart/related MIME email with embedded image
|
174
|
+
|
175
|
+
Sometimes it is desirable to send a document that is made up of many separate
|
176
|
+
parts. For example, an HTML page with embedded images. The multipart/related
|
177
|
+
content type aggregates all the parts and creates the means for the root entity
|
178
|
+
to reference the other entities.
|
179
|
+
|
180
|
+
image = DiscreteMediaFactory.create('/tmp/ruby.png')
|
181
|
+
image.content_transfer_encoding = 'binary'
|
182
|
+
|
183
|
+
html_msg = TextMedia.new(<<-html_data, 'text/html; charset=iso-8859-1')
|
184
|
+
<html>
|
185
|
+
<body>
|
186
|
+
<h1>Ruby Image</h1>
|
187
|
+
<p>Check out this cool pic.</p>
|
188
|
+
<img alt="cool ruby" src="cid:#{image.content_id}"/>
|
189
|
+
<p>Wasn't it cool?</p>
|
190
|
+
</body>
|
191
|
+
</html>
|
192
|
+
html_data
|
193
|
+
html_msg.content_transfer_encoding = '7bit'
|
194
|
+
|
195
|
+
related_msg = MultipartMedia::Related.new
|
196
|
+
related_msg.inline_entity(image)
|
197
|
+
related_msg.add_entity(html_msg)
|
198
|
+
|
199
|
+
email_msg = Message.new(related_msg)
|
200
|
+
email_msg.to = {'joe@domain.com' => 'Joe Schmo'}
|
201
|
+
email_msg.from = {'john@domain.com' => 'John Doe'}
|
202
|
+
email_msg.subject = 'Ruby is cool'
|
203
|
+
email_msg.to_s
|
204
|
+
|
205
|
+
|
206
|
+
=== HTML form with file upload using multipart/form-data encoding
|
207
|
+
|
208
|
+
This example builds a representation of an HTML form that can be POSTed to an
|
209
|
+
HTTP server. It contains a single text input and a file input.
|
210
|
+
|
211
|
+
name_field = TextMedia.new('Joe Blow')
|
212
|
+
|
213
|
+
portrait_filename = '/tmp/joe_portrait.jpg'
|
214
|
+
portrait_field = open(portrait_filename) do |f|
|
215
|
+
ImageMedia.new(f.read, 'image/jpeg') # explicit content type
|
216
|
+
end
|
217
|
+
portrait_field.content_transfer_encoding = 'binary'
|
218
|
+
|
219
|
+
form_data = MultipartMedia::FormData.new
|
220
|
+
form_data.add_entity(name_field, 'name')
|
221
|
+
form_data.add_entity(portrait_field, 'portrait', portrait_filename) # explicity filename
|
222
|
+
form_data.to_s
|
223
|
+
|
224
|
+
|
225
|
+
=== HTML form with file upload using multipart/form-data encoding (DiscreteMediaFactory)
|
226
|
+
|
227
|
+
The outcome of this example is identical to the previous example. The only
|
228
|
+
semantic difference is that the MIME::DiscreteMediaFactory class is used to
|
229
|
+
automatically instantiate the MIME::MediaType object.
|
230
|
+
|
231
|
+
name_field = TextMedia.new('Joe Blow')
|
232
|
+
|
233
|
+
portrait_field = DiscreteMediaFactory.create('/tmp/joe_portrait.jpg') # no explicit content type
|
234
|
+
portrait_field.content_transfer_encoding = 'binary'
|
235
|
+
|
236
|
+
form_data = MultipartMedia::FormData.new
|
237
|
+
form_data.add_entity(name_field, 'name')
|
238
|
+
form_data.add_entity(portrait_field, 'portrait') # no explicit filename
|
239
|
+
form_data.to_s
|
240
|
+
|
241
|
+
|
242
|
+
== More Examples
|
243
|
+
|
244
|
+
For many more examples, check the test class MIMETest.
|
245
|
+
|
246
|
+
|
247
|
+
== Contact
|
248
|
+
|
249
|
+
Please email inquiries to pachl at ecentryx dot com.
|
250
|
+
|
251
|
+
[Home Page] http://mime.rubyforge.org/
|
252
|
+
[RubyForge] http://rubyforge.org/projects/mime/
|
253
|
+
|
254
|
+
== License
|
255
|
+
|
256
|
+
The entire MIME library is free to use under the terms of the Ruby license.
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rake/rdoctask'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
|
5
|
+
gem_spec = Gem::Specification.new do |s|
|
6
|
+
s.name = 'mime'
|
7
|
+
s.version = '0.1'
|
8
|
+
s.summary = 'Multipurpose Internet Mail Extensions (MIME) Library'
|
9
|
+
s.test_files = FileList['test/*.rb']
|
10
|
+
s.files = FileList['README', 'Rakefile', 'lib/**/*.rb', 'test/**/*']
|
11
|
+
s.author = 'Clint Pachl'
|
12
|
+
s.email = 'pachl@ecentryx.com'
|
13
|
+
s.homepage = 'mime.rubyforge.org'
|
14
|
+
s.rubyforge_project = 'mime'
|
15
|
+
s.has_rdoc = true
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
Rake::GemPackageTask.new(gem_spec) do |pkg|
|
20
|
+
pkg.need_tar_gz = true
|
21
|
+
end
|
22
|
+
|
23
|
+
Rake::RDocTask.new do |rd|
|
24
|
+
rd.rdoc_files.include('README', 'lib/')
|
25
|
+
rd.rdoc_dir = 'doc'
|
26
|
+
rd.main = 'README'
|
27
|
+
rd.options << '--all' << '--inline-source'
|
28
|
+
end
|
29
|
+
|
30
|
+
Rake::TestTask.new do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.pattern = 'test/*_test.rb'
|
33
|
+
t.warning = true
|
34
|
+
end
|
data/lib/mime.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#
|
2
|
+
# = Construct Multipurpose Internet Mail Extensions (MIME) messages.
|
3
|
+
#
|
4
|
+
# ---
|
5
|
+
#
|
6
|
+
# RFCs referenced during the implementation of this library:
|
7
|
+
#
|
8
|
+
# * RFC-2822 Internet Message Format (obsoletes 822)
|
9
|
+
# * RFC-2045 MIME Part 1: Format of Internet Message Bodies
|
10
|
+
# * RFC-2046 MIME Part 2: Media Types
|
11
|
+
# * RFC-2047 MIME Part 3: Message Header Extensions for Non-ASCII Text
|
12
|
+
# * RFC-2048 MIME Part 4: Registration Procedures
|
13
|
+
# * RFC-2049 MIME Part 5: Conformance Criteria and Examples
|
14
|
+
#
|
15
|
+
# ---
|
16
|
+
#
|
17
|
+
# See SOAP::MIMEMessage for other implementation ideas.
|
18
|
+
#
|
19
|
+
module MIME
|
20
|
+
|
21
|
+
VERSION = '0.1'
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'mime/content_types'
|
26
|
+
require 'mime/error'
|
27
|
+
require 'mime/header_container'
|
28
|
+
require 'mime/media_type'
|
29
|
+
require 'mime/discrete_media_type'
|
30
|
+
require 'mime/composite_media_type'
|
31
|
+
require 'mime/message'
|
32
|
+
require 'mime/discrete_media_factory'
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module MIME
|
2
|
+
|
3
|
+
#
|
4
|
+
# Composite entities are handled using MIME mechanisms. A MIME processor must
|
5
|
+
# handle the body directly. A CompositeMediaType object is composed of one or
|
6
|
+
# more CompositeMediaType and/or DiscreteMediaType objects.
|
7
|
+
#
|
8
|
+
# This class is abstract.
|
9
|
+
#
|
10
|
+
class CompositeMediaType < MediaType
|
11
|
+
|
12
|
+
def initialize content_type
|
13
|
+
AbstractClassError.no_instantiation(self, CompositeMediaType)
|
14
|
+
|
15
|
+
super(nil, content_type)
|
16
|
+
@entities = Array.new
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# Add a MediaType object to the message.
|
21
|
+
#
|
22
|
+
def add_entity entity
|
23
|
+
raise Error.new('can only add MediaType objects') unless entity.is_a? MediaType
|
24
|
+
@entities.unshift(entity)
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Attach a MediaType object to the message.
|
29
|
+
#
|
30
|
+
def attach_entity entity, params = {}
|
31
|
+
entity.set_content_disposition('attachment', params)
|
32
|
+
add_entity(entity)
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Inline a MediaType object in the message.
|
37
|
+
#
|
38
|
+
def inline_entity entity, params = {}
|
39
|
+
entity.set_content_disposition('inline', params)
|
40
|
+
add_entity(entity)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# MessageMedia is intended to encapsulate another message. In particular,
|
47
|
+
# the <em>message/rfc822</em> content type is used to encapsulate RFC 822
|
48
|
+
# messages.
|
49
|
+
#
|
50
|
+
# TODO Implement
|
51
|
+
#
|
52
|
+
class MessageMedia < CompositeMediaType
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# The abstract base class for all multipart message subtypes. The entities of
|
57
|
+
# a multipart message are delimited by a unique boundary.
|
58
|
+
#
|
59
|
+
class MultipartMedia < CompositeMediaType
|
60
|
+
|
61
|
+
def initialize content_type
|
62
|
+
AbstractClassError.no_instantiation(self, MultipartMedia)
|
63
|
+
super
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# The boundary used to separate the message entities.
|
68
|
+
#
|
69
|
+
def boundary
|
70
|
+
@boundary ||= "Boundary_#{unique_id}"
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Return the multipart representation of the body.
|
75
|
+
#
|
76
|
+
def body
|
77
|
+
all_entities = @entities.join("\r\n--#{boundary}\r\n")
|
78
|
+
"--#{boundary}\r\n#{all_entities}\r\n--#{boundary}--\r\n"
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# The Alternative subtype indicates that each contained entity is an
|
85
|
+
# alternatively formatted version of the same content. The most complex
|
86
|
+
# version should be added to the message first, i.e. it will be sequentially
|
87
|
+
# last in the message.
|
88
|
+
#
|
89
|
+
class MultipartMedia::Alternative < MultipartMedia
|
90
|
+
|
91
|
+
#
|
92
|
+
# Returns a MultipartMedia::Alternative object with a content type of
|
93
|
+
# multipart/alternative.
|
94
|
+
#
|
95
|
+
def initialize
|
96
|
+
super("multipart/alternative; boundary=#{boundary}")
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
#
|
102
|
+
# The FormData subtype expresses values for HTML form data submissions.
|
103
|
+
# ---
|
104
|
+
# RFCs consulted during implementation:
|
105
|
+
#
|
106
|
+
# * RFC-1867 Form-based File Upload in HTML
|
107
|
+
# * RFC-2388 Returning Values from Forms: multipart/form-data
|
108
|
+
#
|
109
|
+
class MultipartMedia::FormData < MultipartMedia
|
110
|
+
|
111
|
+
#
|
112
|
+
# Returns a MultipartMedia::FormData object with a content type of
|
113
|
+
# multipart/form-data.
|
114
|
+
#
|
115
|
+
def initialize
|
116
|
+
super("multipart/form-data; boundary=#{boundary}")
|
117
|
+
end
|
118
|
+
|
119
|
+
#
|
120
|
+
# Add the MediaType object, +entity+, to the FormData object. +name+ is
|
121
|
+
# typically an HTML input tag variable name. If the input tag is of type
|
122
|
+
# _file_, then +filename+ must be specified to indicate a file upload.
|
123
|
+
#
|
124
|
+
def add_entity entity, name, filename = nil
|
125
|
+
entity.set_content_disposition('form-data', 'name' => name, 'filename' => filename)
|
126
|
+
super(entity)
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
#
|
132
|
+
# The Mixed subtype aggregates contextually independent entities.
|
133
|
+
#
|
134
|
+
class MultipartMedia::Mixed < MultipartMedia
|
135
|
+
|
136
|
+
#
|
137
|
+
# Returns a MultipartMedia::Mixed object with a content type of
|
138
|
+
# multipart/mixed.
|
139
|
+
#
|
140
|
+
def initialize
|
141
|
+
super("multipart/mixed; boundary=#{boundary}")
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# The Related subtype aggregates multiple related entities. The message
|
148
|
+
# consists of a root (the first entity) which references subsequent inline
|
149
|
+
# entities. Message entities should be referenced by their Content-ID header.
|
150
|
+
# The syntax of a reference is unspecified and is instead dictated by the
|
151
|
+
# encoding or protocol used in the entity.
|
152
|
+
# ---
|
153
|
+
# RFC consulted during implementation:
|
154
|
+
#
|
155
|
+
# * RFC-2387 The MIME Multipart/Related Content-type
|
156
|
+
#
|
157
|
+
class MultipartMedia::Related < MultipartMedia
|
158
|
+
|
159
|
+
#
|
160
|
+
# Returns a MultipartMedia::Related object with a content type of
|
161
|
+
# multipart/related.
|
162
|
+
#
|
163
|
+
def initialize
|
164
|
+
super("multipart/related; boundary=#{boundary}")
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|