File: body.rb

package info (click to toggle)
ruby-mail 2.8.1%2Bdfsg1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,704 kB
  • sloc: ruby: 73,709; makefile: 3
file content (305 lines) | stat: -rw-r--r-- 9,545 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
# 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", "multipart/alternative" ]
      @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 = ::Mail::Utilities.to_crlf(string.join(''))
        elsif string.respond_to?(:to_s)
          @raw_source = ::Mail::Utilities.to_crlf(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 = default_encoding
      set_charset
    end

    def init_with(coder)
      coder.map.each { |k, v| instance_variable_set(:"@#{k}", v) }
      @parts = Mail::PartsList.new(coder['parts'])
    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', then 'multipart/alternative'
    # 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
    
    def negotiate_best_encoding(message_encoding, allowed_encodings = nil)
      Mail::Encodings::TransferEncoding.negotiate(message_encoding, encoding, raw_source, allowed_encodings)
    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 = nil)
      if multipart?
        self.sort_parts!
        encoded_parts = parts.map { |p| p.encoded }
        ([preamble] + encoded_parts).join(crlf_boundary) + end_boundary + epilogue.to_s
      else
        dec = Mail::Encodings.get_encoding(encoding)
        enc =
          if Utilities.blank?(transfer_encoding)
            dec
          else
            negotiate_best_encoding(transfer_encoding)
          end

        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 = 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 encoding(val = nil)
      if val
        self.encoding = val
      else
        @encoding
      end
    end

    def encoding=( val )
      @encoding =
        if val == "text" || Utilities.blank?(val)
          default_encoding
        else
          val
        end
    end

    # Returns the raw source that the body was initialized with, without
    # any tampering
    attr_reader :raw_source

    # Returns parts of the body
    attr_reader :parts

    # Returns and sets the original character encoding
    attr_accessor :charset

    # Returns and sets the preamble as a string (any text that is before the first MIME boundary)
    attr_accessor :preamble

    # Returns and sets the epilogue as a string (any text that is after the last MIME boundary)
    attr_accessor :epilogue

    # Returns and sets the boundary used by the body
    # Allows you to change the boundary of this Body object
    attr_accessor :boundary

    # Returns true if there are parts defined in the body
    def multipart?
      true unless parts.empty?
    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 ascii_only?
      unless defined? @ascii_only
        @ascii_only = raw_source.ascii_only?
      end
      @ascii_only
    end

    def empty?
      !!raw_source.to_s.empty?
    end

    def default_encoding
      ascii_only? ? '7bit' : '8bit'
    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
      @charset = ascii_only? ? 'US-ASCII' : nil
    end
  end
end