File: body.rb

package info (click to toggle)
ruby-mail 2.6.4%2Bdfsg1-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 4,256 kB
  • ctags: 1,327
  • sloc: ruby: 44,678; makefile: 3
file content (316 lines) | stat: -rw-r--r-- 9,490 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# encoding: utf-8
# frozen_string_literal: true
module Mail
  
  # = Body
  # 
  # The body is where the text of the email is stored.  Mail treats the body
  # as a single object.  The body itself has no information about boundaries
  # used in the MIME standard, it just looks at its content as either a single
  # block of text, or (if it is a multipart message) as an array of blocks of text.
  # 
  # A body has to be told to split itself up into a multipart message by calling
  # #split with the correct boundary.  This is because the body object has no way
  # of knowing what the correct boundary is for itself (there could be many
  # boundaries in a body in the case of a nested MIME text).
  # 
  # Once split is called, Mail::Body will slice itself up on this boundary,
  # assigning anything that appears before the first part to the preamble, and
  # anything that appears after the closing boundary to the epilogue, then
  # each part gets initialized into a Mail::Part object.
  # 
  # The boundary that is used to split up the Body is also stored in the Body
  # object for use on encoding itself back out to a string.  You can 
  # overwrite this if it needs to be changed.
  # 
  # On encoding, the body will return the preamble, then each part joined by
  # the boundary, followed by a closing boundary string and then the epilogue.
  class Body

    def initialize(string = '')
      @boundary = nil
      @preamble = nil
      @epilogue = nil
      @charset  = nil
      @part_sort_order = [ "text/plain", "text/enriched", "text/html" ]
      @parts = Mail::PartsList.new
      if Utilities.blank?(string)
        @raw_source = ''
      else
        # Do join first incase we have been given an Array in Ruby 1.9
        if string.respond_to?(:join)
          @raw_source = string.join('')
        elsif string.respond_to?(:to_s)
          @raw_source = string.to_s
        else
          raise "You can only assign a string or an object that responds_to? :join or :to_s to a body."
        end
      end
      @encoding = (only_us_ascii? ? '7bit' : '8bit')
      set_charset
    end
    
    # Matches this body with another body.  Also matches the decoded value of this
    # body with a string.
    # 
    # Examples:
    # 
    #   body = Mail::Body.new('The body')
    #   body == body #=> true
    #   
    #   body = Mail::Body.new('The body')
    #   body == 'The body' #=> true
    #   
    #   body = Mail::Body.new("VGhlIGJvZHk=\n")
    #   body.encoding = 'base64'
    #   body == "The body" #=> true
    def ==(other)
      if other.class == String
        self.decoded == other
      else
        super
      end
    end
    
    # Accepts a string and performs a regular expression against the decoded text
    # 
    # Examples:
    # 
    #   body = Mail::Body.new('The body')
    #   body =~ /The/ #=> 0
    #   
    #   body = Mail::Body.new("VGhlIGJvZHk=\n")
    #   body.encoding = 'base64'
    #   body =~ /The/ #=> 0
    def =~(regexp)
      self.decoded =~ regexp
    end
    
    # Accepts a string and performs a regular expression against the decoded text
    # 
    # Examples:
    # 
    #   body = Mail::Body.new('The body')
    #   body.match(/The/) #=> #<MatchData "The">
    #   
    #   body = Mail::Body.new("VGhlIGJvZHk=\n")
    #   body.encoding = 'base64'
    #   body.match(/The/) #=> #<MatchData "The">
    def match(regexp)
      self.decoded.match(regexp)
    end

    # Accepts anything that responds to #to_s and checks if it's a substring of the decoded text
    #
    # Examples:
    #
    #   body = Mail::Body.new('The body')
    #   body.include?('The') #=> true
    #
    #   body = Mail::Body.new("VGhlIGJvZHk=\n")
    #   body.encoding = 'base64'
    #   body.include?('The') #=> true
    def include?(other)
      self.decoded.include?(other.to_s)
    end

    # Allows you to set the sort order of the parts, overriding the default sort order.
    # Defaults to 'text/plain', then 'text/enriched', then 'text/html' with any other content
    # type coming after.
    def set_sort_order(order)
      @part_sort_order = order
    end
    
    # Allows you to sort the parts according to the default sort order, or the sort order you
    # set with :set_sort_order.
    #
    # sort_parts! is also called from :encode, so there is no need for you to call this explicitly
    def sort_parts!
      @parts.each do |p|
        p.body.set_sort_order(@part_sort_order)
        p.body.sort_parts!
      end
      @parts.sort!(@part_sort_order)
    end
    
    # Returns the raw source that the body was initialized with, without
    # any tampering
    def raw_source
      @raw_source
    end
   
    def get_best_encoding(target)
      target_encoding = Mail::Encodings.get_encoding(target)
      target_encoding.get_best_compatible(encoding, raw_source)
    end
 
    # Returns a body encoded using transfer_encoding.  Multipart always uses an
    # identiy encoding (i.e. no encoding).
    # Calling this directly is not a good idea, but supported for compatibility
    # TODO: Validate that preamble and epilogue are valid for requested encoding
    def encoded(transfer_encoding = '8bit')
      if multipart?
        self.sort_parts!
        encoded_parts = parts.map { |p| p.encoded }
        ([preamble] + encoded_parts).join(crlf_boundary) + end_boundary + epilogue.to_s
      else
        be = get_best_encoding(transfer_encoding)
        dec = Mail::Encodings::get_encoding(encoding)
        enc = Mail::Encodings::get_encoding(be)
        if dec.nil?
            # Cannot decode, so skip normalization
            raw_source
        else
            # Decode then encode to normalize and allow transforming 
            # from base64 to Q-P and vice versa
            decoded = dec.decode(raw_source)
            if defined?(Encoding) && charset && charset != "US-ASCII"
              decoded.encode!(charset)
              decoded.force_encoding('BINARY') unless Encoding.find(charset).ascii_compatible?
            end
            enc.encode(decoded)
        end
      end
    end
    
    def decoded
      if !Encodings.defined?(encoding)
        raise UnknownEncodingType, "Don't know how to decode #{encoding}, please call #encoded and decode it yourself."
      else
        Encodings.get_encoding(encoding).decode(raw_source)
      end
    end
    
    def to_s
      decoded
    end
    
    def charset
      @charset
    end
    
    def charset=( val )
      @charset = val
    end

    def encoding(val = nil)
      if val
        self.encoding = val
      else
        @encoding
      end
    end
    
    def encoding=( val )
      @encoding = if val == "text" || Utilities.blank?(val)
          (only_us_ascii? ? '7bit' : '8bit')
      else
          val
      end
    end

    # Returns the preamble (any text that is before the first MIME boundary)
    def preamble
      @preamble
    end

    # Sets the preamble to a string (adds text before the first MIME boundary)
    def preamble=( val )
      @preamble = val
    end
    
    # Returns the epilogue (any text that is after the last MIME boundary)
    def epilogue
      @epilogue
    end
    
    # Sets the epilogue to a string (adds text after the last MIME boundary)
    def epilogue=( val )
      @epilogue = val
    end
    
    # Returns true if there are parts defined in the body
    def multipart?
      true unless parts.empty?
    end
    
    # Returns the boundary used by the body
    def boundary
      @boundary
    end
    
    # Allows you to change the boundary of this Body object
    def boundary=( val )
      @boundary = val
    end

    def parts
      @parts
    end
    
    def <<( val )
      if @parts
        @parts << val
      else
        @parts = Mail::PartsList.new[val]
      end
    end

    def split!(boundary)
      self.boundary = boundary
      parts = extract_parts

      # Make the preamble equal to the preamble (if any)
      self.preamble = parts[0].to_s.strip
      # Make the epilogue equal to the epilogue (if any)
      self.epilogue = parts[-1].to_s.strip
      parts[1...-1].to_a.each { |part| @parts << Mail::Part.new(part) }
      self
    end

    def only_us_ascii?
      !(raw_source =~ /[^\x01-\x7f]/)
    end
    
    def empty?
      !!raw_source.to_s.empty?
    end
    
    private

    # split parts by boundary, ignore first part if empty, append final part when closing boundary was missing
    def extract_parts
      parts_regex = /
        (?:                    # non-capturing group
          \A                |  # start of string OR
          \r\n                 # line break
         )
        (
          --#{Regexp.escape(boundary || "")}  # boundary delimiter
          (?:--)?                             # with non-capturing optional closing
        )
        (?=\s*$)                              # lookahead matching zero or more spaces followed by line-ending
      /x
      parts = raw_source.split(parts_regex).each_slice(2).to_a
      parts.each_with_index { |(part, _), index| parts.delete_at(index) if index > 0 && Utilities.blank?(part) }

      if parts.size > 1
        final_separator = parts[-2][1]
        parts << [""] if final_separator != "--#{boundary}--"
      end
      parts.map(&:first)
    end
    
    def crlf_boundary
      "\r\n--#{boundary}\r\n"
    end
    
    def end_boundary
      "\r\n--#{boundary}--\r\n"
    end
    
    def set_charset
      only_us_ascii? ? @charset = 'US-ASCII' : @charset = nil
    end
  end
end