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
@@ -0,0 +1,96 @@
|
|
1
|
+
module MIME
|
2
|
+
|
3
|
+
#
|
4
|
+
# Handles MIME content type detection using file name extension.
|
5
|
+
#
|
6
|
+
# === See Also
|
7
|
+
#
|
8
|
+
# * http://w3schools.com/media/media_mimeref.asp for more content types
|
9
|
+
# * rubygem MIME::Types for extensive content type lookup
|
10
|
+
#
|
11
|
+
module ContentTypes
|
12
|
+
|
13
|
+
#
|
14
|
+
# Content types are in the form of <em>media-type/sub-type</em>. Each
|
15
|
+
# content type has one or more file extentions associated with it.
|
16
|
+
#
|
17
|
+
# The content types below are listed in alphabetical order and the
|
18
|
+
# extention arrays are in order of preference.
|
19
|
+
#
|
20
|
+
# The following content types were stolen from the NGiNX webserver. NGiNX
|
21
|
+
# is great server, check it out at http://nginx.net.
|
22
|
+
#
|
23
|
+
CONTENT_TYPES = {
|
24
|
+
'application/atom+xml' => %w(atom),
|
25
|
+
'application/java-archive' => %w(jar war ear),
|
26
|
+
'application/mac-binhex40' => %w(hqx),
|
27
|
+
'application/msword' => %w(doc),
|
28
|
+
'application/octet-stream' => %w(bin exe dll deb dmg eot iso img msi msp msm),
|
29
|
+
'application/pdf' => %w(pdf),
|
30
|
+
'application/postscript' => %w(ps eps ai),
|
31
|
+
'application/rtf' => %w(rtf),
|
32
|
+
'application/vnd.ms-excel' => %w(xls),
|
33
|
+
'application/vnd.ms-powerpoint' => %w(ppt),
|
34
|
+
'application/vnd.wap.wmlc' => %w(wmlc),
|
35
|
+
'application/vnd.wap.xhtml+xml' => %w(xhtml),
|
36
|
+
'application/x-cocoa' => %w(cco),
|
37
|
+
'application/x-java-archive-diff' => %w(jardiff),
|
38
|
+
'application/x-java-jnlp-file' => %w(jnlp),
|
39
|
+
'application/x-javascript' => %w(js),
|
40
|
+
'application/x-makeself' => %w(run),
|
41
|
+
'application/x-perl' => %w(pl pm),
|
42
|
+
'application/x-pilot' => %w(prc pdb),
|
43
|
+
'application/x-rar-compressed' => %w(rar),
|
44
|
+
'application/x-redhat-package-manager' => %w(rpm),
|
45
|
+
'application/x-sea' => %w(sea),
|
46
|
+
'application/x-shockwave-flash' => %w(swf),
|
47
|
+
'application/x-stuffit' => %w(sit),
|
48
|
+
'application/x-tcl' => %w(tcl tk),
|
49
|
+
'application/x-x509-ca-cert' => %w(crt pem der),
|
50
|
+
'application/x-xpinstall' => %w(xpi),
|
51
|
+
'application/zip' => %w(zip),
|
52
|
+
|
53
|
+
'audio/midi' => %w(mid midi kar),
|
54
|
+
'audio/mpeg' => %w(mp3),
|
55
|
+
'audio/x-realaudio' => %w(ra),
|
56
|
+
|
57
|
+
'image/gif' => %w(gif),
|
58
|
+
'image/jpeg' => %w(jpg jpeg),
|
59
|
+
'image/png' => %w(png),
|
60
|
+
'image/tiff' => %w(tif tiff),
|
61
|
+
'image/vnd.wap.wbmp' => %w(wbmp),
|
62
|
+
'image/x-icon' => %w(ico),
|
63
|
+
'image/x-jng' => %w(jng),
|
64
|
+
'image/x-ms-bmp' => %w(bmp),
|
65
|
+
'image/svg+xml' => %w(svg),
|
66
|
+
|
67
|
+
'text/css' => %w(css),
|
68
|
+
'text/html' => %w(html htm shtml),
|
69
|
+
'text/mathml' => %w(mml),
|
70
|
+
'text/plain' => %w(txt),
|
71
|
+
'text/x-component' => %w(htc),
|
72
|
+
'text/xml' => %w(xml rss),
|
73
|
+
|
74
|
+
'video/3gpp' => %w(3gpp 3gp),
|
75
|
+
'video/mpeg' => %w(mpeg mpg),
|
76
|
+
'video/quicktime' => %w(mov),
|
77
|
+
'video/x-flv' => %w(flv),
|
78
|
+
'video/x-mng' => %w(mng),
|
79
|
+
'video/x-ms-asf' => %w(asx asf),
|
80
|
+
'video/x-ms-wmv' => %w(wmv),
|
81
|
+
'video/x-msvideo' => %w(avi)
|
82
|
+
}
|
83
|
+
|
84
|
+
#
|
85
|
+
# Return the MIME content type of the +file+ path or object.
|
86
|
+
#
|
87
|
+
def file_type file
|
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
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module MIME
|
2
|
+
|
3
|
+
#
|
4
|
+
# Class object used only for initializing derived DiscreteMediaType objects.
|
5
|
+
#
|
6
|
+
class DiscreteMediaFactory
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
include ContentTypes
|
11
|
+
|
12
|
+
#
|
13
|
+
# Creates a corresponding DiscreteMediaType subclass object for the given
|
14
|
+
# +file+ based on +file+'s filename extension. +file+ can be a file path
|
15
|
+
# or File object.
|
16
|
+
#
|
17
|
+
# +content_type+ can be specified in order to override the auto detected
|
18
|
+
# content type. If the +content_type+ cannot be detected, an
|
19
|
+
# UnknownContentError exception will be raised.
|
20
|
+
#
|
21
|
+
# Creates and sets the singleton method +path+ on the created object. The
|
22
|
+
# +path+ method is utilized by other methods in the MIME library,
|
23
|
+
# therefore, eliminating redundant and explicit filename assignments.
|
24
|
+
#
|
25
|
+
# ==== Comparison Example
|
26
|
+
#
|
27
|
+
# entity1 = open('/tmp/file1.txt')
|
28
|
+
# entity2 = DiscreteMediaFactory.create('/tmp/file2.txt')
|
29
|
+
#
|
30
|
+
# mixed_msg = Multipart::Mixed.new
|
31
|
+
# mixed_msg.attach_entity(entity1.read, entity.path)
|
32
|
+
# mixed_msg.attach_entity(entity2) # no path needed
|
33
|
+
#
|
34
|
+
def create file, content_type = nil
|
35
|
+
if file.is_a? File
|
36
|
+
cntnt = file.read
|
37
|
+
ctype = content_type || file_type(file.path)
|
38
|
+
fname = file.path
|
39
|
+
else
|
40
|
+
cntnt = IO.read(file)
|
41
|
+
ctype = content_type || file_type(file)
|
42
|
+
fname = file
|
43
|
+
end
|
44
|
+
|
45
|
+
raise UnknownContentError unless ctype
|
46
|
+
|
47
|
+
media_obj =
|
48
|
+
case ctype.match(/^(\w+)\//)[1]
|
49
|
+
when 'application'; ApplicationMedia.new(cntnt, ctype)
|
50
|
+
when 'audio' ; AudioMedia.new(cntnt, ctype)
|
51
|
+
when 'image' ; ImageMedia.new(cntnt, ctype)
|
52
|
+
when 'text' ; TextMedia.new(cntnt, ctype)
|
53
|
+
when 'video' ; VideoMedia.new(cntnt, ctype)
|
54
|
+
end
|
55
|
+
|
56
|
+
class << media_obj; attr_accessor :path end
|
57
|
+
media_obj.path = fname
|
58
|
+
media_obj
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize
|
64
|
+
AbstractClassError.no_instantiation self, DiscreteMediaFactory
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module MIME
|
2
|
+
|
3
|
+
#
|
4
|
+
# Discrete media types must be handled by non-MIME mechanisms; they are
|
5
|
+
# opaque to MIME processors. Therefore, the body of a DiscreteMediaType
|
6
|
+
# object does not need further MIME processing.
|
7
|
+
#
|
8
|
+
# This class is abstract.
|
9
|
+
#
|
10
|
+
class DiscreteMediaType < MediaType
|
11
|
+
|
12
|
+
def initialize body, content_type = 'application/octet-stream'
|
13
|
+
AbstractClassError.no_instantiation(self, DiscreteMediaType)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
#
|
20
|
+
# ApplicationMedia is intended for discrete data that is to be processed by
|
21
|
+
# some type of application program. The body contains information which must
|
22
|
+
# be processed by an application before it is viewable or usable by a user.
|
23
|
+
#
|
24
|
+
# ApplicationMedia is the catch all class. If your content cannot be
|
25
|
+
# identified as another DiscreteMediaType, then it is application media.
|
26
|
+
#
|
27
|
+
# See DiscreteMediaType.new for initialization parameters.
|
28
|
+
#
|
29
|
+
class ApplicationMedia < DiscreteMediaType
|
30
|
+
end
|
31
|
+
|
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.
|
37
|
+
#
|
38
|
+
class AudioMedia < DiscreteMediaType
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
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.
|
46
|
+
#
|
47
|
+
class ImageMedia < DiscreteMediaType
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# TextMedia is intended for content which is principally textual in form.
|
52
|
+
#
|
53
|
+
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'
|
63
|
+
super
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# VideoMedia is intended for discrete video content. The content subtype
|
70
|
+
# indicates the specific video format. The RFC describes video media as
|
71
|
+
# content that contains a time-varying-picture image, possibly with color and
|
72
|
+
# coordinated sound.
|
73
|
+
#
|
74
|
+
# See DiscreteMediaType.new for initialization parameters.
|
75
|
+
#
|
76
|
+
class VideoMedia < DiscreteMediaType
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/lib/mime/error.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module MIME
|
2
|
+
|
3
|
+
#
|
4
|
+
# General purpose MIME errors.
|
5
|
+
#
|
6
|
+
class Error < StandardError ; end
|
7
|
+
|
8
|
+
#
|
9
|
+
# Undetectable MIME content type error.
|
10
|
+
#
|
11
|
+
class UnknownContentError < Error ; end
|
12
|
+
|
13
|
+
#
|
14
|
+
# Abstract class object initialization error. Many classes in the MIME
|
15
|
+
# library are abstract and will raise this error.
|
16
|
+
#
|
17
|
+
class AbstractClassError < Error
|
18
|
+
|
19
|
+
#
|
20
|
+
# A helper method for detecting the intialization of an object in an
|
21
|
+
# abstract class. +myself+ must always be *self* and +klass+ is the
|
22
|
+
# abstract class object.
|
23
|
+
#
|
24
|
+
def self.no_instantiation myself, klass
|
25
|
+
if myself.class == klass
|
26
|
+
raise AbstractClassError.new('cannot construct abstract class')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module MIME
|
2
|
+
|
3
|
+
#
|
4
|
+
# Storage for RFC 2822 headers.
|
5
|
+
#
|
6
|
+
class HeaderContainer
|
7
|
+
|
8
|
+
#
|
9
|
+
# Return a new header container object.
|
10
|
+
#
|
11
|
+
def initialize
|
12
|
+
@headers = Hash.new
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Convert all headers to their string equivalents and join them using the
|
17
|
+
# RFC 2822 CRLF line separator.
|
18
|
+
#
|
19
|
+
def to_s
|
20
|
+
@headers.collect do |name, value|
|
21
|
+
"#{name}: #{value}"
|
22
|
+
end.join("\r\n")
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Add the +name+/+value+ pair to the header container.
|
27
|
+
#
|
28
|
+
def add name, value
|
29
|
+
@headers.store(name, value)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module MIME
|
2
|
+
module Headers
|
3
|
+
|
4
|
+
#
|
5
|
+
# The RFC 2822 Internet message header fields.
|
6
|
+
#
|
7
|
+
module Internet
|
8
|
+
|
9
|
+
attr_reader :date, :from, :to
|
10
|
+
attr_reader :cc, :bcc, :reply_to, :message_id
|
11
|
+
attr_reader :comments, :keywords, :subject
|
12
|
+
|
13
|
+
|
14
|
+
#
|
15
|
+
# Required Headers
|
16
|
+
#
|
17
|
+
|
18
|
+
def date= date
|
19
|
+
@date = date
|
20
|
+
headers.add('Date', @date)
|
21
|
+
end
|
22
|
+
|
23
|
+
def from= list
|
24
|
+
@from = stringify_email_list(list)
|
25
|
+
headers.add('From', @from)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to= list
|
29
|
+
@to = stringify_email_list(list)
|
30
|
+
headers.add('To', @to)
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
#
|
35
|
+
# Optional Headers
|
36
|
+
#
|
37
|
+
|
38
|
+
def cc= list
|
39
|
+
@cc = stringify_email_list(list)
|
40
|
+
headers.add('Cc', @cc)
|
41
|
+
end
|
42
|
+
|
43
|
+
def bcc= list
|
44
|
+
@bcc = stringify_email_list(list)
|
45
|
+
headers.add('Bcc', @bcc)
|
46
|
+
end
|
47
|
+
|
48
|
+
def reply_to= list
|
49
|
+
@reply_to = stringify_email_list(list)
|
50
|
+
headers.add('Reply-To', @reply_to)
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# The message +id+ must contain an embedded "@" symbol. An example +id+
|
55
|
+
# might be <em>some-unique-id@domain.com</em>.
|
56
|
+
#
|
57
|
+
def message_id= id
|
58
|
+
@message_id = "<#{id}>"
|
59
|
+
headers.add('Message-ID', @message_id)
|
60
|
+
end
|
61
|
+
|
62
|
+
def comments= comments
|
63
|
+
@comments = comments
|
64
|
+
headers.add('Comments', @comments)
|
65
|
+
end
|
66
|
+
|
67
|
+
def keywords= keywords
|
68
|
+
@keywords = keywords
|
69
|
+
headers.add('Keywords', @keywords)
|
70
|
+
end
|
71
|
+
|
72
|
+
def subject= subject
|
73
|
+
@subject = subject
|
74
|
+
headers.add('Subject', @subject)
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
#
|
81
|
+
# +list+ may be a single email address or a Hash of _email_ => _name_
|
82
|
+
# pairs. Set _name_ to nil when it is unknown.
|
83
|
+
#
|
84
|
+
def stringify_email_list list
|
85
|
+
list.map {|email, name| name ? "#{name} <#{email}>" : email}.join(', ')
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module MIME
|
2
|
+
module Headers
|
3
|
+
|
4
|
+
#
|
5
|
+
# The RFC 2045 MIME message header fields.
|
6
|
+
#
|
7
|
+
module MIME
|
8
|
+
|
9
|
+
attr_reader :mime_version,
|
10
|
+
:content_description,
|
11
|
+
:content_disposition,
|
12
|
+
:content_id,
|
13
|
+
:content_transfer_encoding,
|
14
|
+
:content_type
|
15
|
+
|
16
|
+
|
17
|
+
#
|
18
|
+
# Describes the content, which can be useful for non-MIME clients.
|
19
|
+
#
|
20
|
+
def content_description= description
|
21
|
+
@content_description = description
|
22
|
+
headers.add('Content-Description', description)
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Specifies the disposition of the content relative to its enclosing
|
27
|
+
# message. Valid values for +disposition+ are _inline_ and _attachment_.
|
28
|
+
# Parameters can also be specified here; see the RFC for details.
|
29
|
+
#
|
30
|
+
# RFC 2183 Communicating Presentation Information in Internet Messages.
|
31
|
+
#
|
32
|
+
def content_disposition= disposition
|
33
|
+
@content_disposition = disposition
|
34
|
+
headers.add('Content-Disposition', disposition)
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Globally unique ID that identifies a top-level message or message
|
39
|
+
# entity. Content IDs can be used for referencing or caching purposes.
|
40
|
+
#
|
41
|
+
def content_id= id
|
42
|
+
@content_id = id
|
43
|
+
headers.add('Content-ID', "<#{id}>")
|
44
|
+
end
|
45
|
+
|
46
|
+
#
|
47
|
+
# The mechanism used for encoding the top-level message content.
|
48
|
+
#
|
49
|
+
# Common Encoding Mechanisms
|
50
|
+
# 1. 7bit
|
51
|
+
# 2. 8bit
|
52
|
+
# 3. binary
|
53
|
+
# 4. quoted-printable
|
54
|
+
# 5. base64
|
55
|
+
#
|
56
|
+
def content_transfer_encoding= encoding
|
57
|
+
@content_transfer_encoding = encoding
|
58
|
+
headers.add('Content-Transfer-Encoding', encoding)
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Specifies the media type and subtype of the content. +type+ will have
|
63
|
+
# the form <em>media-type/subtype</em>.
|
64
|
+
#
|
65
|
+
# Common Content Types
|
66
|
+
# 1. application/octet-stream
|
67
|
+
# 2. audio/mpeg
|
68
|
+
# 3. image/jpeg
|
69
|
+
# 4. text/plain
|
70
|
+
# 5. video/mpeg
|
71
|
+
#
|
72
|
+
def content_type= type
|
73
|
+
@content_type = type
|
74
|
+
headers.add('Content-Type', type)
|
75
|
+
end
|
76
|
+
|
77
|
+
#
|
78
|
+
# Currently only version 1.0 exists.
|
79
|
+
#
|
80
|
+
def mime_version= version
|
81
|
+
@mime_version = version
|
82
|
+
headers.add('MIME-Version', version)
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
#
|
88
|
+
# +type+ is the disposition type of either "inline" or "attachment".
|
89
|
+
# +params+ is a Hash with zero or more of the following keys:
|
90
|
+
#
|
91
|
+
# * filename => name of file
|
92
|
+
# * creation-date => RFC2822 data-time
|
93
|
+
# * modification-date => RFC2822 data-time
|
94
|
+
# * read-date => RFC2822 data-time
|
95
|
+
# * size => file size in octets
|
96
|
+
#
|
97
|
+
# The values for the *-date keys may use Time::rfc2822.
|
98
|
+
#
|
99
|
+
def set_content_disposition type, params = {}
|
100
|
+
disposition = type
|
101
|
+
|
102
|
+
if params['filename']
|
103
|
+
params['filename'] = File.basename(params['filename'])
|
104
|
+
elsif self.respond_to?(:path)
|
105
|
+
params['filename'] = File.basename(self.path)
|
106
|
+
end
|
107
|
+
|
108
|
+
params.each do |name, value|
|
109
|
+
disposition << %Q[; #{name}="#{value}"] if value
|
110
|
+
end
|
111
|
+
|
112
|
+
self.content_disposition = disposition
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|