rmail 0.17
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/NEWS +309 -0
- data/NOTES +14 -0
- data/README +83 -0
- data/THANKS +25 -0
- data/TODO +112 -0
- data/guide/Intro.txt +122 -0
- data/guide/MIME.txt +6 -0
- data/guide/TableOfContents.txt +13 -0
- data/install.rb +1023 -0
- data/lib/rmail.rb +50 -0
- data/lib/rmail/address.rb +829 -0
- data/lib/rmail/header.rb +987 -0
- data/lib/rmail/mailbox.rb +62 -0
- data/lib/rmail/mailbox/mboxreader.rb +182 -0
- data/lib/rmail/message.rb +201 -0
- data/lib/rmail/parser.rb +412 -0
- data/lib/rmail/parser/multipart.rb +217 -0
- data/lib/rmail/parser/pushbackreader.rb +173 -0
- data/lib/rmail/serialize.rb +190 -0
- data/lib/rmail/utils.rb +59 -0
- data/rmail.gemspec +17 -0
- data/tests/addrgrammar.txt +113 -0
- data/tests/data/mbox.odd +4 -0
- data/tests/data/mbox.simple +8 -0
- data/tests/data/multipart/data.1 +5 -0
- data/tests/data/multipart/data.10 +1 -0
- data/tests/data/multipart/data.11 +9 -0
- data/tests/data/multipart/data.12 +9 -0
- data/tests/data/multipart/data.13 +3 -0
- data/tests/data/multipart/data.14 +3 -0
- data/tests/data/multipart/data.15 +3 -0
- data/tests/data/multipart/data.16 +3 -0
- data/tests/data/multipart/data.17 +0 -0
- data/tests/data/multipart/data.2 +5 -0
- data/tests/data/multipart/data.3 +2 -0
- data/tests/data/multipart/data.4 +3 -0
- data/tests/data/multipart/data.5 +1 -0
- data/tests/data/multipart/data.6 +2 -0
- data/tests/data/multipart/data.7 +3 -0
- data/tests/data/multipart/data.8 +5 -0
- data/tests/data/multipart/data.9 +4 -0
- data/tests/data/parser.badmime1 +4 -0
- data/tests/data/parser.badmime2 +6 -0
- data/tests/data/parser.nested-multipart +75 -0
- data/tests/data/parser.nested-simple +12 -0
- data/tests/data/parser.nested-simple2 +16 -0
- data/tests/data/parser.nested-simple3 +21 -0
- data/tests/data/parser.rfc822 +65 -0
- data/tests/data/parser.simple-mime +24 -0
- data/tests/data/parser/multipart.1 +8 -0
- data/tests/data/parser/multipart.10 +4 -0
- data/tests/data/parser/multipart.11 +12 -0
- data/tests/data/parser/multipart.12 +12 -0
- data/tests/data/parser/multipart.13 +6 -0
- data/tests/data/parser/multipart.14 +6 -0
- data/tests/data/parser/multipart.15 +6 -0
- data/tests/data/parser/multipart.16 +6 -0
- data/tests/data/parser/multipart.2 +8 -0
- data/tests/data/parser/multipart.3 +5 -0
- data/tests/data/parser/multipart.4 +6 -0
- data/tests/data/parser/multipart.5 +4 -0
- data/tests/data/parser/multipart.6 +5 -0
- data/tests/data/parser/multipart.7 +6 -0
- data/tests/data/parser/multipart.8 +8 -0
- data/tests/data/parser/multipart.9 +7 -0
- data/tests/data/transparency/absolute.1 +5 -0
- data/tests/data/transparency/absolute.2 +1 -0
- data/tests/data/transparency/absolute.3 +2 -0
- data/tests/data/transparency/absolute.4 +3 -0
- data/tests/data/transparency/absolute.5 +4 -0
- data/tests/data/transparency/absolute.6 +49 -0
- data/tests/data/transparency/message.1 +73 -0
- data/tests/data/transparency/message.2 +34 -0
- data/tests/data/transparency/message.3 +63 -0
- data/tests/data/transparency/message.4 +5 -0
- data/tests/data/transparency/message.5 +15 -0
- data/tests/data/transparency/message.6 +1185 -0
- data/tests/runtests.rb +35 -0
- data/tests/testaddress.rb +1192 -0
- data/tests/testbase.rb +207 -0
- data/tests/testheader.rb +1207 -0
- data/tests/testmailbox.rb +47 -0
- data/tests/testmboxreader.rb +161 -0
- data/tests/testmessage.rb +257 -0
- data/tests/testparser.rb +634 -0
- data/tests/testparsermultipart.rb +205 -0
- data/tests/testpushbackreader.rb +40 -0
- data/tests/testserialize.rb +264 -0
- data/tests/testtestbase.rb +112 -0
- data/tests/testtranspparency.rb +105 -0
- metadata +143 -0
@@ -0,0 +1,217 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C) 2002, 2003, 2004 Matt Armstrong. All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
6
|
+
#
|
7
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer.
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer in the
|
11
|
+
# documentation and/or other materials provided with the distribution.
|
12
|
+
# 3. The name of the author may not be used to endorse or promote products
|
13
|
+
# derived from this software without specific prior written permission.
|
14
|
+
#
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
16
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
17
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
18
|
+
# NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
19
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
20
|
+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
21
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
22
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
23
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
24
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
25
|
+
#
|
26
|
+
#++
|
27
|
+
# Implements the RMail::Parser::MultipartReader class.
|
28
|
+
|
29
|
+
module RMail
|
30
|
+
class Parser
|
31
|
+
|
32
|
+
require 'rmail/parser/pushbackreader'
|
33
|
+
|
34
|
+
# A simple interface to facilitate parsing a multipart message.
|
35
|
+
#
|
36
|
+
# The typical RubyMail user will have no use for this class.
|
37
|
+
# Although it is an example of how to use a PushbackReader, the
|
38
|
+
# typical RubyMail user will never use a PushbackReader either.
|
39
|
+
# ;-)
|
40
|
+
class MultipartReader < PushbackReader # :nodoc:
|
41
|
+
|
42
|
+
# Creates a MIME multipart parser.
|
43
|
+
#
|
44
|
+
# +input+ is an object supporting a +read+ method that takes one
|
45
|
+
# argument, a suggested number of bytes to read, and returns
|
46
|
+
# either a string of bytes read or nil if there are no more
|
47
|
+
# bytes to read.
|
48
|
+
#
|
49
|
+
# +boundary+ is the string boundary that separates the parts,
|
50
|
+
# without the "--" prefix.
|
51
|
+
#
|
52
|
+
# This class is a suitable input source when parsing recursive
|
53
|
+
# multiparts.
|
54
|
+
def initialize(input, boundary)
|
55
|
+
super(input)
|
56
|
+
escaped = Regexp.escape(boundary)
|
57
|
+
@delimiter_re = /(?:\G|\n)--#{escaped}(--)?\s*?(\n|\z)/
|
58
|
+
@might_be_delimiter_re = might_be_delimiter_re(boundary)
|
59
|
+
@caryover = nil
|
60
|
+
@chunks = []
|
61
|
+
@eof = false
|
62
|
+
@delimiter = nil
|
63
|
+
@delimiter_is_last = false
|
64
|
+
@in_epilogue = false
|
65
|
+
@in_preamble = true
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the next chunk of data from the input stream as a
|
69
|
+
# string. The chunk_size is passed down to the read method of
|
70
|
+
# this object's input stream to suggest a size to it, but don't
|
71
|
+
# depend on the returned data being of any particular size.
|
72
|
+
#
|
73
|
+
# If this method returns nil, you must call next_part to begin
|
74
|
+
# reading the next MIME part in the data stream.
|
75
|
+
def read_chunk(chunk_size)
|
76
|
+
chunk = read_chunk_low(chunk_size)
|
77
|
+
if chunk
|
78
|
+
if @in_epilogue
|
79
|
+
while more = read_chunk_low(chunk_size)
|
80
|
+
chunk << more
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
chunk
|
85
|
+
end
|
86
|
+
|
87
|
+
def read_chunk_low(chunk_size)
|
88
|
+
|
89
|
+
if @pushback
|
90
|
+
return standard_read_chunk(chunk_size)
|
91
|
+
end
|
92
|
+
|
93
|
+
input_gave_nil = false
|
94
|
+
loop {
|
95
|
+
return nil if @eof || @delimiter
|
96
|
+
|
97
|
+
unless @chunks.empty?
|
98
|
+
chunk, @delimiter, @delimiter_is_last = @chunks.shift
|
99
|
+
return chunk
|
100
|
+
end
|
101
|
+
|
102
|
+
chunk = standard_read_chunk(chunk_size)
|
103
|
+
|
104
|
+
if chunk.nil?
|
105
|
+
input_gave_nil = true
|
106
|
+
end
|
107
|
+
if @caryover
|
108
|
+
if chunk
|
109
|
+
@caryover << chunk
|
110
|
+
end
|
111
|
+
chunk = @caryover
|
112
|
+
@caryover = nil
|
113
|
+
end
|
114
|
+
|
115
|
+
if chunk.nil?
|
116
|
+
@eof = true
|
117
|
+
return nil
|
118
|
+
elsif @in_epilogue
|
119
|
+
return chunk
|
120
|
+
end
|
121
|
+
|
122
|
+
start = 0
|
123
|
+
found_last_delimiter = false
|
124
|
+
|
125
|
+
while !found_last_delimiter and
|
126
|
+
(start < chunk.length) and
|
127
|
+
(found = chunk.index(@delimiter_re, start))
|
128
|
+
|
129
|
+
if $~[2] == '' and !input_gave_nil
|
130
|
+
break
|
131
|
+
end
|
132
|
+
|
133
|
+
delimiter = $~[0]
|
134
|
+
|
135
|
+
# check if delimiter had the trailing --
|
136
|
+
if $~.begin(1)
|
137
|
+
found_last_delimiter = true
|
138
|
+
end
|
139
|
+
|
140
|
+
temp = if found == start
|
141
|
+
nil
|
142
|
+
else
|
143
|
+
chunk[start, found - start]
|
144
|
+
end
|
145
|
+
|
146
|
+
@chunks << [ temp, delimiter, found_last_delimiter ]
|
147
|
+
|
148
|
+
start = $~.end(0)
|
149
|
+
end
|
150
|
+
|
151
|
+
chunk = chunk[start..-1] if start > 0
|
152
|
+
|
153
|
+
# If something that looks like a delimiter exists at the end
|
154
|
+
# of this chunk, refrain from returning it.
|
155
|
+
unless found_last_delimiter or input_gave_nil
|
156
|
+
start = chunk.rindex(/\n/) || 0
|
157
|
+
if chunk.index(@might_be_delimiter_re, start)
|
158
|
+
@caryover = chunk[start..-1]
|
159
|
+
chunk[start..-1] = ''
|
160
|
+
chunk = nil if chunk.length == 0
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
unless chunk.nil?
|
165
|
+
@chunks << [ chunk, nil, false ]
|
166
|
+
end
|
167
|
+
chunk, @delimiter, @delimiter_is_last = @chunks.shift
|
168
|
+
|
169
|
+
if chunk
|
170
|
+
return chunk
|
171
|
+
end
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
# Start reading the next part. Returns true if there is a next
|
176
|
+
# part to read, or false if we have reached the end of the file.
|
177
|
+
def next_part
|
178
|
+
if @eof
|
179
|
+
false
|
180
|
+
else
|
181
|
+
if @delimiter
|
182
|
+
@delimiter = nil
|
183
|
+
@in_preamble = false
|
184
|
+
@in_epilogue = @delimiter_is_last
|
185
|
+
end
|
186
|
+
true
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Call this to determine if #read is currently returning strings
|
191
|
+
# from the preamble portion of a mime multipart.
|
192
|
+
def preamble?
|
193
|
+
@in_preamble
|
194
|
+
end
|
195
|
+
|
196
|
+
# Call this to determine if #read is currently returning strings
|
197
|
+
# from the epilogue portion of a mime multipart.
|
198
|
+
def epilogue?
|
199
|
+
@in_epilogue
|
200
|
+
end
|
201
|
+
|
202
|
+
# Call this to retrieve the delimiter string that terminated the
|
203
|
+
# part just read. This is cleared by #next_part.
|
204
|
+
def delimiter
|
205
|
+
@delimiter
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def might_be_delimiter_re(boundary)
|
211
|
+
s = PushbackReader.maybe_contains_re("--" + boundary)
|
212
|
+
Regexp.new('(?:\A|\n)(?:' + s + '|\z)')
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2002, 2003 Matt Armstrong. All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
6
|
+
#
|
7
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer.
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer in the
|
11
|
+
# documentation and/or other materials provided with the distribution.
|
12
|
+
# 3. The name of the author may not be used to endorse or promote products
|
13
|
+
# derived from this software without specific prior written permission.
|
14
|
+
#
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
16
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
17
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
18
|
+
# NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
19
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
20
|
+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
21
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
22
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
23
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
24
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
25
|
+
#
|
26
|
+
#++
|
27
|
+
# Implements the RMail::Parser::PushbackReader class.
|
28
|
+
|
29
|
+
module RMail
|
30
|
+
class Parser
|
31
|
+
|
32
|
+
class Error < StandardError; end
|
33
|
+
|
34
|
+
# A utility class for reading from an input source in an efficient
|
35
|
+
# chunked manner.
|
36
|
+
#
|
37
|
+
# The idea is to read data in descent sized chunks (the default is
|
38
|
+
# 16k), but provide a way to "push back" some of the chunk if we
|
39
|
+
# read too much.
|
40
|
+
#
|
41
|
+
# This class is useful only as a base class for other readers --
|
42
|
+
# e.g. a reader that parses MIME multipart documents, or a reader
|
43
|
+
# that understands one or more mailbox formats.
|
44
|
+
#
|
45
|
+
# The typical RubyMail user will have no interest in this class.
|
46
|
+
# ;-)
|
47
|
+
class PushbackReader # :nodoc:
|
48
|
+
|
49
|
+
# Create a PushbackReader and have it read from a given input
|
50
|
+
# source.
|
51
|
+
#
|
52
|
+
# The input source must either be a String or respond to the
|
53
|
+
# "read" method in the same way as an IO object.
|
54
|
+
def initialize(input)
|
55
|
+
unless defined? input.read(1)
|
56
|
+
unless input.is_a?(String)
|
57
|
+
raise ArgumentError, "input object not IO or String"
|
58
|
+
end
|
59
|
+
@pushback = input
|
60
|
+
@input = nil
|
61
|
+
else
|
62
|
+
@pushback = nil
|
63
|
+
@input = input
|
64
|
+
end
|
65
|
+
@chunk_size = 16384
|
66
|
+
end
|
67
|
+
|
68
|
+
# Read a chunk of input. The "size" argument is just a
|
69
|
+
# suggestion, and more or fewer bytes may be returned. If
|
70
|
+
# "size" is nil, then return the entire rest of the input
|
71
|
+
# stream.
|
72
|
+
def read(size = @chunk_size)
|
73
|
+
case size
|
74
|
+
when nil
|
75
|
+
chunk = nil
|
76
|
+
while temp = read(@chunk_size)
|
77
|
+
if chunk
|
78
|
+
chunk << temp
|
79
|
+
else
|
80
|
+
chunk = temp
|
81
|
+
end
|
82
|
+
end
|
83
|
+
chunk
|
84
|
+
when Fixnum
|
85
|
+
read_chunk(size)
|
86
|
+
else
|
87
|
+
raise ArgumentError,
|
88
|
+
"Read size (#{size.inspect}) must be a Fixnum or nil."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Read a chunk of a given size. Unlike #read, #read_chunk must
|
93
|
+
# be passed a chunk size, and cannot be passed nil.
|
94
|
+
#
|
95
|
+
# This is the function that should be re-defined in subclasses
|
96
|
+
# for specialized behavior.
|
97
|
+
def read_chunk(size)
|
98
|
+
standard_read_chunk(size)
|
99
|
+
end
|
100
|
+
|
101
|
+
# The standard implementation of read_chunk. This can be
|
102
|
+
# convenient to call from derived classes when super() isn't
|
103
|
+
# easy to use.
|
104
|
+
def standard_read_chunk(size)
|
105
|
+
unless size.is_a?(Fixnum) && size > 0
|
106
|
+
raise ArgumentError,
|
107
|
+
"Read size (#{size.inspect}) must be greater than 0."
|
108
|
+
end
|
109
|
+
if @pushback
|
110
|
+
chunk = @pushback
|
111
|
+
@pushback = nil
|
112
|
+
elsif ! @input.nil?
|
113
|
+
chunk = @input.read(size)
|
114
|
+
end
|
115
|
+
return chunk
|
116
|
+
end
|
117
|
+
|
118
|
+
# Push a string back. This will be the next chunk of data
|
119
|
+
# returned by #read.
|
120
|
+
#
|
121
|
+
# Because it has not been needed and would compromise
|
122
|
+
# efficiency, only one chunk of data can be pushed back between
|
123
|
+
# successive calls to #read.
|
124
|
+
def pushback(string)
|
125
|
+
raise RMail::Parser::Error,
|
126
|
+
'You have already pushed a string back.' if @pushback
|
127
|
+
@pushback = string
|
128
|
+
end
|
129
|
+
|
130
|
+
# Retrieve the chunk size of this reader.
|
131
|
+
attr_reader :chunk_size
|
132
|
+
|
133
|
+
# Set the chunk size of this reader in bytes. This is useful
|
134
|
+
# mainly for testing, though perhaps some operations could be
|
135
|
+
# optimized by tweaking this value. The chunk size must be a
|
136
|
+
# Fixnum greater than 0.
|
137
|
+
def chunk_size=(size)
|
138
|
+
unless size.is_a?(Fixnum)
|
139
|
+
raise ArgumentError, "chunk size must be a Fixnum"
|
140
|
+
end
|
141
|
+
unless size >= 1
|
142
|
+
raise ArgumentError, "invalid size #{size.inspect} given"
|
143
|
+
end
|
144
|
+
@chunk_size = size
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns true if the next call to read_chunk will return nil.
|
148
|
+
def eof
|
149
|
+
@pushback.nil? and (@input.nil? or @input.eof)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Creates a regexp that'll match the given boundary string in
|
153
|
+
# its entirely anywhere in a string, or any partial prefix of
|
154
|
+
# the boundary string so long as the match is anchored at the
|
155
|
+
# end of the string. This is useful for various subclasses of
|
156
|
+
# PushbackReader that need to know if a given input chunk might
|
157
|
+
# contain (or contain just the beginning of) an interesting
|
158
|
+
# string.
|
159
|
+
def self.maybe_contains_re(boundary)
|
160
|
+
left = Regexp.quote(boundary[0,1])
|
161
|
+
right = ''
|
162
|
+
boundary[1..-1].each_byte { |ch|
|
163
|
+
left << '(?:'
|
164
|
+
left << Regexp.quote(ch.chr)
|
165
|
+
right << '|\z)'
|
166
|
+
}
|
167
|
+
left + right
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (C) 2002, 2003 Matt Armstrong. All rights reserved.
|
3
|
+
#
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
6
|
+
#
|
7
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer.
|
9
|
+
# 2. Redistributions in binary form must reproduce the above copyright
|
10
|
+
# notice, this list of conditions and the following disclaimer in the
|
11
|
+
# documentation and/or other materials provided with the distribution.
|
12
|
+
# 3. The name of the author may not be used to endorse or promote products
|
13
|
+
# derived from this software without specific prior written permission.
|
14
|
+
#
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
16
|
+
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
17
|
+
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
|
18
|
+
# NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
19
|
+
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
20
|
+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
21
|
+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
22
|
+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
23
|
+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
24
|
+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
25
|
+
#
|
26
|
+
#++
|
27
|
+
# Implements the RMail::Serialize class.
|
28
|
+
|
29
|
+
module RMail
|
30
|
+
|
31
|
+
# The RMail::Serialize class writes an RMail::Message object into an
|
32
|
+
# IO object or string. The result is a standard mail message in
|
33
|
+
# text form.
|
34
|
+
#
|
35
|
+
# To do this, you pass the RMail::Message object to the
|
36
|
+
# RMail::Serialize object. RMail::Serialize can write into any
|
37
|
+
# object supporting the << method.
|
38
|
+
#
|
39
|
+
# As a convenience, RMail::Serialize.write is a class method you can
|
40
|
+
# use directly:
|
41
|
+
#
|
42
|
+
# # Write to a file
|
43
|
+
# File.open('my-message', 'w') { |f|
|
44
|
+
# RMail::Serialize.write(f, message)
|
45
|
+
# }
|
46
|
+
#
|
47
|
+
# # Write to a new string
|
48
|
+
# string = RMail::Serialize.write('', message)
|
49
|
+
class Serialize
|
50
|
+
|
51
|
+
@@boundary_count = 0
|
52
|
+
|
53
|
+
# Initialize this Serialize object with an output stream. If
|
54
|
+
# escape_from is not nil, lines with a leading From are escaped.
|
55
|
+
def initialize(output, escape_from = nil)
|
56
|
+
@output = output
|
57
|
+
@escape_from = escape_from
|
58
|
+
end
|
59
|
+
|
60
|
+
# Serialize a given message into this object's output object.
|
61
|
+
def serialize(message)
|
62
|
+
calculate_boundaries(message) if message.multipart?
|
63
|
+
serialize_low(message)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Serialize a message into a given output object. The output
|
67
|
+
# object must support the << method in the same way that an IO or
|
68
|
+
# String object does.
|
69
|
+
def Serialize.write(output, message)
|
70
|
+
Serialize.new(output).serialize(message)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def serialize_low(message, depth = 0)
|
76
|
+
if message.multipart?
|
77
|
+
delimiters, delimiters_boundary = message.get_delimiters
|
78
|
+
unless delimiters
|
79
|
+
boundary = "\n--" + message.header.param('Content-Type', 'boundary')
|
80
|
+
delimiters = Array.new(message.body.length + 1, boundary + "\n")
|
81
|
+
delimiters[-1] = boundary + "--\n"
|
82
|
+
end
|
83
|
+
|
84
|
+
@output << message.header.to_s
|
85
|
+
|
86
|
+
if message.body.length > 0 or message.preamble or
|
87
|
+
delimiters.last.length > 0
|
88
|
+
@output << "\n"
|
89
|
+
end
|
90
|
+
|
91
|
+
if message.preamble
|
92
|
+
@output << message.preamble
|
93
|
+
end
|
94
|
+
|
95
|
+
delimiter = 0
|
96
|
+
message.each_part { |part|
|
97
|
+
@output << delimiters[delimiter]
|
98
|
+
delimiter = delimiter.succ
|
99
|
+
serialize_low(part, depth + 1)
|
100
|
+
}
|
101
|
+
|
102
|
+
@output << delimiters[delimiter]
|
103
|
+
|
104
|
+
if message.epilogue
|
105
|
+
@output << message.epilogue
|
106
|
+
end
|
107
|
+
|
108
|
+
else
|
109
|
+
@output << message.header.to_s
|
110
|
+
unless message.body.nil?
|
111
|
+
@output << "\n"
|
112
|
+
@output << message.body
|
113
|
+
if depth == 0 and message.body.length > 0 and
|
114
|
+
message.body[-1] != ?\n
|
115
|
+
@output << "\n"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
@output
|
120
|
+
end
|
121
|
+
|
122
|
+
# Walk the multipart tree and make sure the boundaries generated
|
123
|
+
# will actually work.
|
124
|
+
def calculate_boundaries(message)
|
125
|
+
calculate_boundaries_low(message, [])
|
126
|
+
unless message.header['MIME-Version']
|
127
|
+
message.header['MIME-Version'] = "1.0"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def calculate_boundaries_low(part, boundaries)
|
132
|
+
# First, come up with a candidate boundary for this part and
|
133
|
+
# save it in our list of boundaries.
|
134
|
+
boundary = make_and_set_unique_boundary(part, boundaries)
|
135
|
+
|
136
|
+
# Now walk through each part and make sure the boundaries are
|
137
|
+
# suitable. We dup the boundaries array before recursing since
|
138
|
+
# sibling multipart can re-use boundary strings (though it isn't
|
139
|
+
# a good idea).
|
140
|
+
boundaries.push(boundary)
|
141
|
+
part.each_part { |p|
|
142
|
+
calculate_boundaries_low(p, boundaries) if p.multipart?
|
143
|
+
}
|
144
|
+
boundaries.pop
|
145
|
+
end
|
146
|
+
|
147
|
+
# Generate a random boundary
|
148
|
+
def generate_boundary
|
149
|
+
@@boundary_count += 1
|
150
|
+
t = Time.now
|
151
|
+
sprintf("=-%d-%d-%d-%d-%d-=",
|
152
|
+
t.tv_sec.to_s,
|
153
|
+
t.tv_usec.to_s,
|
154
|
+
Process.pid.to_s,
|
155
|
+
rand(10000),
|
156
|
+
@@boundary_count)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns a boundary that will probably work out. Extracts any
|
160
|
+
# existing boundary from the header, but will generate a default
|
161
|
+
# one if the header doesn't have one set yet.
|
162
|
+
def make_and_set_unique_boundary(part, boundaries)
|
163
|
+
candidate = part.header.param('content-type', 'boundary')
|
164
|
+
unique = make_unique_boundary(candidate || generate_boundary, boundaries)
|
165
|
+
if candidate.nil? or candidate != unique
|
166
|
+
part.header.set_boundary(unique)
|
167
|
+
end
|
168
|
+
unique
|
169
|
+
end
|
170
|
+
|
171
|
+
# Make the passed boundary unique among the passed boundaries and
|
172
|
+
# return it.
|
173
|
+
def make_unique_boundary(boundary, boundaries)
|
174
|
+
continue = true
|
175
|
+
while continue
|
176
|
+
continue = false
|
177
|
+
boundaries.each do |existing|
|
178
|
+
if boundary == existing[0, boundary.length]
|
179
|
+
continue = true
|
180
|
+
break
|
181
|
+
end
|
182
|
+
end
|
183
|
+
break unless continue
|
184
|
+
boundary = generate_boundary
|
185
|
+
end
|
186
|
+
boundary
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|