rmail-sup 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/NEWS +323 -0
  3. data/NOTES +14 -0
  4. data/README +83 -0
  5. data/Rakefile +184 -0
  6. data/THANKS +25 -0
  7. data/TODO +115 -0
  8. data/guide/Intro.txt +122 -0
  9. data/guide/MIME.txt +6 -0
  10. data/guide/TableOfContents.txt +13 -0
  11. data/install.rb +1023 -0
  12. data/lib/rmail.rb +50 -0
  13. data/lib/rmail/address.rb +841 -0
  14. data/lib/rmail/header.rb +981 -0
  15. data/lib/rmail/mailbox.rb +62 -0
  16. data/lib/rmail/mailbox/mboxreader.rb +182 -0
  17. data/lib/rmail/message.rb +201 -0
  18. data/lib/rmail/parser.rb +412 -0
  19. data/lib/rmail/parser/multipart.rb +217 -0
  20. data/lib/rmail/parser/pushbackreader.rb +173 -0
  21. data/lib/rmail/serialize.rb +190 -0
  22. data/lib/rmail/utils.rb +59 -0
  23. data/test/addrgrammar.txt +113 -0
  24. data/test/data/mbox.odd +4 -0
  25. data/test/data/mbox.simple +8 -0
  26. data/test/data/multipart/data.1 +5 -0
  27. data/test/data/multipart/data.10 +1 -0
  28. data/test/data/multipart/data.11 +9 -0
  29. data/test/data/multipart/data.12 +9 -0
  30. data/test/data/multipart/data.13 +3 -0
  31. data/test/data/multipart/data.14 +3 -0
  32. data/test/data/multipart/data.15 +3 -0
  33. data/test/data/multipart/data.16 +3 -0
  34. data/test/data/multipart/data.17 +0 -0
  35. data/test/data/multipart/data.2 +5 -0
  36. data/test/data/multipart/data.3 +2 -0
  37. data/test/data/multipart/data.4 +3 -0
  38. data/test/data/multipart/data.5 +1 -0
  39. data/test/data/multipart/data.6 +2 -0
  40. data/test/data/multipart/data.7 +3 -0
  41. data/test/data/multipart/data.8 +5 -0
  42. data/test/data/multipart/data.9 +4 -0
  43. data/test/data/parser.badmime1 +4 -0
  44. data/test/data/parser.badmime2 +6 -0
  45. data/test/data/parser.nested-multipart +75 -0
  46. data/test/data/parser.nested-simple +12 -0
  47. data/test/data/parser.nested-simple2 +16 -0
  48. data/test/data/parser.nested-simple3 +21 -0
  49. data/test/data/parser.rfc822 +65 -0
  50. data/test/data/parser.simple-mime +24 -0
  51. data/test/data/parser/multipart.1 +8 -0
  52. data/test/data/parser/multipart.10 +4 -0
  53. data/test/data/parser/multipart.11 +12 -0
  54. data/test/data/parser/multipart.12 +12 -0
  55. data/test/data/parser/multipart.13 +6 -0
  56. data/test/data/parser/multipart.14 +6 -0
  57. data/test/data/parser/multipart.15 +6 -0
  58. data/test/data/parser/multipart.16 +6 -0
  59. data/test/data/parser/multipart.2 +8 -0
  60. data/test/data/parser/multipart.3 +5 -0
  61. data/test/data/parser/multipart.4 +6 -0
  62. data/test/data/parser/multipart.5 +4 -0
  63. data/test/data/parser/multipart.6 +5 -0
  64. data/test/data/parser/multipart.7 +6 -0
  65. data/test/data/parser/multipart.8 +8 -0
  66. data/test/data/parser/multipart.9 +7 -0
  67. data/test/data/transparency/absolute.1 +5 -0
  68. data/test/data/transparency/absolute.2 +1 -0
  69. data/test/data/transparency/absolute.3 +2 -0
  70. data/test/data/transparency/absolute.4 +3 -0
  71. data/test/data/transparency/absolute.5 +4 -0
  72. data/test/data/transparency/absolute.6 +49 -0
  73. data/test/data/transparency/message.1 +73 -0
  74. data/test/data/transparency/message.2 +34 -0
  75. data/test/data/transparency/message.3 +63 -0
  76. data/test/data/transparency/message.4 +5 -0
  77. data/test/data/transparency/message.5 +15 -0
  78. data/test/data/transparency/message.6 +1185 -0
  79. data/test/runtests.rb +35 -0
  80. data/test/testaddress.rb +1204 -0
  81. data/test/testbase.rb +204 -0
  82. data/test/testheader.rb +1225 -0
  83. data/test/testmailbox.rb +47 -0
  84. data/test/testmboxreader.rb +161 -0
  85. data/test/testmessage.rb +257 -0
  86. data/test/testparser.rb +634 -0
  87. data/test/testparsermultipart.rb +205 -0
  88. data/test/testpushbackreader.rb +40 -0
  89. data/test/testserialize.rb +264 -0
  90. data/test/testtestbase.rb +116 -0
  91. data/test/testtranspparency.rb +105 -0
  92. data/version +1 -0
  93. 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