mime 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|