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