Class: Mime

Inherits:
Object
  • Object
show all
Defined in:
lib/mime.rb

Overview

Introduction

A basic mime class for really basic and probably non-standard parsing and construction of MIME messages.

Intended for two main purposes in this project:

  1. As the container that is used to build up the message for eventual serialization as an eml.

  2. For assistance in parsing the transport_message_headers provided in .msg files, which are then kept through to the final eml.

TODO

  • Better streaming support, rather than an all-in-string approach.

  • Add OrderedHash optionally, to not lose ordering in headers.

  • A fair bit remains to be done for this class, its fairly immature. But generally I’d like to see it be more generally useful.

  • All sorts of correctness issues, encoding particular.

  • Duplication of work in net/http.rb’s HTTPHeader? Don’t know if the overlap is sufficient. I don’t want to lower case things, just for starters.

  • Mime was the original place I wrote #to_tree, intended as a quick debug hack.

Constant Summary collapse

Hash =
begin
	require 'orderedhash'
	OrderedHash
rescue LoadError
	Hash
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(str, ignore_body = false) ⇒ Mime

Create a Mime object using str as an initial serialization, which must contain headers and a body (even if empty). Needs work.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/mime.rb', line 36

def initialize str, ignore_body=false
	headers, @body = $~[1..-1] if str[/(.*?\r?\n)(?:\r?\n(.*))?\Z/m]

	@headers = Hash.new { |hash, key| hash[key] = [] }
	@body ||= ''
	headers.to_s.scan(/^\S+:\s*.*(?:\n\t.*)*/).each do |header|
		@headers[header[/(\S+):/, 1]] << header[/\S+:\s*(.*)/m, 1].gsub(/\s+/m, ' ').strip # this is kind of wrong
	end

	# don't have to have content type i suppose
	@content_type, attrs = nil, {}
	if content_type = @headers['Content-Type'][0]
		@content_type, attrs = Mime.split_header content_type
	end

	return if ignore_body

	if multipart?
		if body.empty?
			@preamble = ''
			@epilogue = ''
			@parts = []
		else
			# we need to split the message at the boundary
			boundary = attrs['boundary'] or raise "no boundary for multipart message"

			# splitting the body:
			parts = body.split(/--#{Regexp.quote boundary}/m)
			unless parts[-1] =~ /^--/; warn "bad multipart boundary (missing trailing --)"
			else parts[-1][0..1] = ''
			end
			parts.each_with_index do |part, i|
				part =~ /^(\r?\n)?(.*?)(\r?\n)?\Z/m
				part.replace $2
				warn "bad multipart boundary" if (1...parts.length-1) === i and !($1 && $3)
			end
			@preamble = parts.shift
			@epilogue = parts.pop
			@parts = parts.map { |part| Mime.new part }
		end
	end
end

Instance Attribute Details

#bodyObject (readonly)

Returns the value of attribute body.



32
33
34
# File 'lib/mime.rb', line 32

def body
  @body
end

#content_typeObject (readonly)

Returns the value of attribute content_type.



32
33
34
# File 'lib/mime.rb', line 32

def content_type
  @content_type
end

#epilogueObject (readonly)

Returns the value of attribute epilogue.



32
33
34
# File 'lib/mime.rb', line 32

def epilogue
  @epilogue
end

#headersObject (readonly)

Returns the value of attribute headers.



32
33
34
# File 'lib/mime.rb', line 32

def headers
  @headers
end

#partsObject (readonly)

Returns the value of attribute parts.



32
33
34
# File 'lib/mime.rb', line 32

def parts
  @parts
end

#preambleObject (readonly)

Returns the value of attribute preamble.



32
33
34
# File 'lib/mime.rb', line 32

def preamble
  @preamble
end

Class Method Details

.make_boundary(i, extra_obj = Mime) ⇒ Object

i is some value that should be unique for all multipart boundaries for a given message



139
140
141
# File 'lib/mime.rb', line 139

def self.make_boundary i, extra_obj = Mime
	"----_=_NextPart_#{'%03d' % i}_#{'%08x' % extra_obj.object_id}.#{'%08x' % Time.now}"
end

.split_header(header) ⇒ Object



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/mime.rb', line 121

def self.split_header header
	# FIXME: haven't read standard. not sure what its supposed to do with " in the name, or if other
	# escapes are allowed. can't test on windows as " isn't allowed anyway. can be fixed with more
	# accurate parser later.
	# maybe move to some sort of Header class. but not all headers should be of it i suppose.
	# at least add a join_header then, taking name and {}. for use in Mime#to_s (for boundary
	# rewrite), and Attachment#to_mime, among others...
	attrs = {}
	header.scan(/;\s*([^\s=]+)\s*=\s*("[^"]*"|[^\s;]*)\s*/m).each do |key, value|
		if attrs[key]; warn "ignoring duplicate header attribute #{key.inspect}"
		else attrs[key] = value[/^"/] ? value[1..-2] : value
		end
	end

	[header[/^[^;]+/].strip, attrs]
end

Instance Method Details

#inspectObject



83
84
85
86
# File 'lib/mime.rb', line 83

def inspect
	# add some extra here.
	"#<Mime content_type=#{@content_type.inspect}>"
end

#multipart?Boolean

Returns:

  • (Boolean)


79
80
81
# File 'lib/mime.rb', line 79

def multipart?
	@content_type && @content_type =~ /^multipart/ ? true : false
end

#to_s(opts = {}) ⇒ Object



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/mime.rb', line 103

def to_s opts={}
	opts = {:boundary_counter => 0}.merge opts
	if multipart?
		boundary = Mime.make_boundary opts[:boundary_counter] += 1, self
		@body = [preamble, parts.map { |part| "\r\n" + part.to_s(opts) + "\r\n" }, "--\r\n" + epilogue].
			flatten.join("\r\n--" + boundary)
		content_type, attrs = Mime.split_header @headers['Content-Type'][0]
		attrs['boundary'] = boundary
		@headers['Content-Type'] = [([content_type] + attrs.map { |key, val| %{#{key}="#{val}"} }).join('; ')]
	end

	str = ''
	@headers.each do |key, vals|
		vals.each { |val| str << "#{key}: #{val}\r\n" }
	end
	str << "\r\n" + @body
end

#to_treeObject



88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/mime.rb', line 88

def to_tree
	if multipart?
		str = "- #{inspect}\n"
		parts.each_with_index do |part, i|
			last = i == parts.length - 1
			part.to_tree.split(/\n/).each_with_index do |line, j|
				str << "  #{last ? (j == 0 ? "\\" : ' ') : '|'}" + line + "\n"
			end
		end
		str
	else
		"- #{inspect}\n"
	end
end