rmail-sup 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/NEWS +323 -0
- data/NOTES +14 -0
- data/README +83 -0
- data/Rakefile +184 -0
- data/THANKS +25 -0
- data/TODO +115 -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 +841 -0
- data/lib/rmail/header.rb +981 -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/test/addrgrammar.txt +113 -0
- data/test/data/mbox.odd +4 -0
- data/test/data/mbox.simple +8 -0
- data/test/data/multipart/data.1 +5 -0
- data/test/data/multipart/data.10 +1 -0
- data/test/data/multipart/data.11 +9 -0
- data/test/data/multipart/data.12 +9 -0
- data/test/data/multipart/data.13 +3 -0
- data/test/data/multipart/data.14 +3 -0
- data/test/data/multipart/data.15 +3 -0
- data/test/data/multipart/data.16 +3 -0
- data/test/data/multipart/data.17 +0 -0
- data/test/data/multipart/data.2 +5 -0
- data/test/data/multipart/data.3 +2 -0
- data/test/data/multipart/data.4 +3 -0
- data/test/data/multipart/data.5 +1 -0
- data/test/data/multipart/data.6 +2 -0
- data/test/data/multipart/data.7 +3 -0
- data/test/data/multipart/data.8 +5 -0
- data/test/data/multipart/data.9 +4 -0
- data/test/data/parser.badmime1 +4 -0
- data/test/data/parser.badmime2 +6 -0
- data/test/data/parser.nested-multipart +75 -0
- data/test/data/parser.nested-simple +12 -0
- data/test/data/parser.nested-simple2 +16 -0
- data/test/data/parser.nested-simple3 +21 -0
- data/test/data/parser.rfc822 +65 -0
- data/test/data/parser.simple-mime +24 -0
- data/test/data/parser/multipart.1 +8 -0
- data/test/data/parser/multipart.10 +4 -0
- data/test/data/parser/multipart.11 +12 -0
- data/test/data/parser/multipart.12 +12 -0
- data/test/data/parser/multipart.13 +6 -0
- data/test/data/parser/multipart.14 +6 -0
- data/test/data/parser/multipart.15 +6 -0
- data/test/data/parser/multipart.16 +6 -0
- data/test/data/parser/multipart.2 +8 -0
- data/test/data/parser/multipart.3 +5 -0
- data/test/data/parser/multipart.4 +6 -0
- data/test/data/parser/multipart.5 +4 -0
- data/test/data/parser/multipart.6 +5 -0
- data/test/data/parser/multipart.7 +6 -0
- data/test/data/parser/multipart.8 +8 -0
- data/test/data/parser/multipart.9 +7 -0
- data/test/data/transparency/absolute.1 +5 -0
- data/test/data/transparency/absolute.2 +1 -0
- data/test/data/transparency/absolute.3 +2 -0
- data/test/data/transparency/absolute.4 +3 -0
- data/test/data/transparency/absolute.5 +4 -0
- data/test/data/transparency/absolute.6 +49 -0
- data/test/data/transparency/message.1 +73 -0
- data/test/data/transparency/message.2 +34 -0
- data/test/data/transparency/message.3 +63 -0
- data/test/data/transparency/message.4 +5 -0
- data/test/data/transparency/message.5 +15 -0
- data/test/data/transparency/message.6 +1185 -0
- data/test/runtests.rb +35 -0
- data/test/testaddress.rb +1204 -0
- data/test/testbase.rb +204 -0
- data/test/testheader.rb +1225 -0
- data/test/testmailbox.rb +47 -0
- data/test/testmboxreader.rb +161 -0
- data/test/testmessage.rb +257 -0
- data/test/testparser.rb +634 -0
- data/test/testparsermultipart.rb +205 -0
- data/test/testpushbackreader.rb +40 -0
- data/test/testserialize.rb +264 -0
- data/test/testtestbase.rb +116 -0
- data/test/testtranspparency.rb +105 -0
- data/version +1 -0
- metadata +149 -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
|