sisimai 4.23.0 → 4.24.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sisimai might be problematic. Click here for more details.

Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/ANALYTICAL-PRECISION +31 -21
  3. data/ChangeLog.md +20 -1
  4. data/README-JA.md +4 -4
  5. data/README.md +4 -4
  6. data/lib/sisimai/address.rb +1 -3
  7. data/lib/sisimai/bite/email/activehunter.rb +1 -0
  8. data/lib/sisimai/bite/email/amazonses.rb +2 -4
  9. data/lib/sisimai/bite/email/amazonworkmail.rb +5 -9
  10. data/lib/sisimai/bite/email/apachejames.rb +8 -4
  11. data/lib/sisimai/bite/email/exim.rb +3 -7
  12. data/lib/sisimai/bite/email/google.rb +1 -3
  13. data/lib/sisimai/bite/email/gsuite.rb +1 -0
  14. data/lib/sisimai/bite/email/interscanmss.rb +1 -1
  15. data/lib/sisimai/bite/email/mailru.rb +3 -7
  16. data/lib/sisimai/bite/email/mxlogic.rb +3 -7
  17. data/lib/sisimai/bite/email/qmail.rb +3 -6
  18. data/lib/sisimai/bite/email/x4.rb +3 -6
  19. data/lib/sisimai/bite/email/yahoo.rb +2 -4
  20. data/lib/sisimai/bite/json/amazonses.rb +2 -4
  21. data/lib/sisimai/data.rb +13 -23
  22. data/lib/sisimai/message/email.rb +24 -39
  23. data/lib/sisimai/mime.rb +214 -38
  24. data/lib/sisimai/reason.rb +1 -3
  25. data/lib/sisimai/reason/blocked.rb +2 -0
  26. data/lib/sisimai/rfc3464.rb +8 -12
  27. data/lib/sisimai/rfc5322.rb +1 -3
  28. data/lib/sisimai/string.rb +8 -6
  29. data/lib/sisimai/version.rb +1 -1
  30. data/set-of-emails/maildir/bsd/email-apachejames-01.eml +1 -2
  31. data/set-of-emails/maildir/bsd/{rfc3464-02.eml → email-domino-03.eml} +0 -0
  32. data/set-of-emails/maildir/bsd/email-office365-08.eml +49 -51
  33. data/set-of-emails/maildir/bsd/email-outlook-08.eml +3 -2
  34. data/set-of-emails/maildir/bsd/email-sendmail-56.eml +86 -0
  35. data/set-of-emails/maildir/bsd/email-verizon-02.eml +1 -2
  36. metadata +4 -4
  37. data/lib/sisimai/rfc2606.rb +0 -23
@@ -95,10 +95,8 @@ module Sisimai::Bite::Email
95
95
  # Remote host said: 550 5.1.1 <kijitora@example.org>... User Unknown [RCPT_TO]
96
96
  v['diagnosis'] = e
97
97
 
98
- if cv = e.match(/\[([A-Z]{4}).*\]\z/)
99
- # Get SMTP command from the value of "Remote host said:"
100
- v['command'] = cv[1]
101
- end
98
+ # Get SMTP command from the value of "Remote host said:"
99
+ if cv = e.match(/\[([A-Z]{4}).*\]\z/) then v['command'] = cv[1] end
102
100
  else
103
101
  # <mailboxfull@example.jp>:
104
102
  # Remote host said:
@@ -149,10 +149,8 @@ module Sisimai::Bite::JSON
149
149
  v['diagnosis'] = e['diagnosticCode']
150
150
  end
151
151
 
152
- if cv = o['reportingMTA'].match(/\Adsn;[ ](.+)\z/)
153
- # 'reportingMTA' => 'dsn; a27-23.smtp-out.us-west-2.amazonses.com',
154
- v['lhost'] = cv[1]
155
- end
152
+ # 'reportingMTA' => 'dsn; a27-23.smtp-out.us-west-2.amazonses.com',
153
+ if cv = o['reportingMTA'].match(/\Adsn;[ ](.+)\z/) then v['lhost'] = cv[1] end
156
154
 
157
155
  if BounceType.key?(o['bounceType'].to_sym) &&
158
156
  BounceType[o['bounceType'].to_sym].key?(o['bounceSubType'].to_sym)
data/lib/sisimai/data.rb CHANGED
@@ -94,7 +94,6 @@ module Sisimai
94
94
  return nil unless data.is_a? Sisimai::Message
95
95
 
96
96
  messageobj = data
97
- mailheader = data.header
98
97
  rfc822data = messageobj.rfc822
99
98
  fieldorder = { :recipient => [], :addresser => [] }
100
99
  objectlist = []
@@ -197,14 +196,12 @@ module Sisimai
197
196
  break if datestring
198
197
  end
199
198
 
200
- if datestring
199
+ if datestring && cv = datestring.match(/\A(.+)[ ]+([-+]\d{4})\z/)
201
200
  # Get the value of timezone offset from datestring
202
- if cv = datestring.match(/\A(.+)[ ]+([-+]\d{4})\z/)
203
- # Wed, 26 Feb 2014 06:05:48 -0500
204
- datestring = cv[1]
205
- zoneoffset = Sisimai::DateTime.tz2second(cv[2])
206
- p['timezoneoffset'] = cv[2]
207
- end
201
+ # Wed, 26 Feb 2014 06:05:48 -0500
202
+ datestring = cv[1]
203
+ zoneoffset = Sisimai::DateTime.tz2second(cv[2])
204
+ p['timezoneoffset'] = cv[2]
208
205
  end
209
206
 
210
207
  begin
@@ -218,7 +215,7 @@ module Sisimai
218
215
  next unless p['timestamp']
219
216
 
220
217
  # OTHER_TEXT_HEADERS:
221
- recvheader = mailheader['received'] || []
218
+ recvheader = data.header['received'] || []
222
219
  unless recvheader.empty?
223
220
  # Get localhost and remote host name from Received header.
224
221
  %w[lhost rhost].each { |a| e[a] ||= '' }
@@ -244,11 +241,8 @@ module Sisimai
244
241
  # The value of "List-Id" header
245
242
  p['listid'] = rfc822data['list-id'] || ''
246
243
  unless p['listid'].empty?
247
- # Get the value of List-Id header
248
- if cv = p['listid'].match(/\A.*([<].+[>]).*\z/)
249
- # List name <list-id@example.org>
250
- p['listid'] = cv[1]
251
- end
244
+ # Get the value of List-Id header like "List name <list-id@example.org>"
245
+ if cv = p['listid'].match(/\A.*([<].+[>]).*\z/) then p['listid'] = cv[1] end
252
246
  p['listid'] = p['listid'].delete('<>').chomp("\r")
253
247
  p['listid'] = '' if p['listid'].include?(' ')
254
248
  end
@@ -256,11 +250,9 @@ module Sisimai
256
250
  # The value of "Message-Id" header
257
251
  p['messageid'] = rfc822data['message-id'] || ''
258
252
  unless p['messageid'].empty?
259
- # Remove angle brackets
260
- if cv = p['messageid'].match(/\A([^ ]+)[ ].*/)
261
- p['messageid'] = cv[1]
262
- end
263
- p['messageid'] = p['messageid'].delete('<>').chomp("\r")
253
+ # Leave only string inside of angle brackets(<>)
254
+ if cv = p['messageid'].match(/\A([^ ]+)[ ].*/) then p['messageid'] = cv[1] end
255
+ if cv = p['messageid'].match(/[<]([^ ]+?)[>]/) then p['messageid'] = cv[1] end
264
256
  end
265
257
 
266
258
  # CHECK_DELIVERY_STATUS_VALUE:
@@ -307,10 +299,8 @@ module Sisimai
307
299
 
308
300
  # Check the value of "action"
309
301
  if p['action'].size > 0
310
- if cv = p['action'].match(/\A(.+?) .+/)
311
- # Action: expanded (to multi-recipient alias)
312
- p['action'] = cv[1]
313
- end
302
+ # Action: expanded (to multi-recipient alias)
303
+ if cv = p['action'].match(/\A(.+?) .+/) then p['action'] = cv[1] end
314
304
 
315
305
  unless %w[failed delayed delivered relayed expanded].index(p['action'])
316
306
  # The value of "action" is not in the following values:
@@ -29,7 +29,6 @@ module Sisimai
29
29
  DefaultSet = Sisimai::Order::Email.another
30
30
  SubjectTab = Sisimai::Order::Email.by('subject')
31
31
  ExtHeaders = Sisimai::Order::Email.headers
32
- ReEncoding = Sisimai::MIME.patterns
33
32
 
34
33
  # Make data structure from the email message(a body part and headers)
35
34
  # @param [Hash] argvs Email data
@@ -392,22 +391,34 @@ module Sisimai
392
391
  mailheader['subject'] ||= ''
393
392
  mailheader['content-type'] ||= ''
394
393
 
394
+ if hookmethod.is_a? Proc
395
+ # Call the hook method
396
+ begin
397
+ p = {
398
+ 'datasrc' => 'email',
399
+ 'headers' => mailheader,
400
+ 'message' => bodystring,
401
+ 'bounces' => nil
402
+ }
403
+ havecaught = hookmethod.call(p)
404
+ rescue StandardError => ce
405
+ warn ' ***warning: Something is wrong in hook method :' << ce.to_s
406
+ end
407
+ end
408
+
395
409
  # Decode BASE64 Encoded message body, rewrite.
396
410
  mesgformat = (mailheader['content-type'] || '').downcase
397
411
  ctencoding = (mailheader['content-transfer-encoding'] || '').downcase
398
412
 
399
413
  if mesgformat.start_with?('text/plain', 'text/html')
400
414
  # Content-Type: text/plain; charset=UTF-8
401
- if ctencoding == 'base64' || ctencoding == 'quoted-printable'
415
+ if ctencoding == 'base64'
402
416
  # Content-Transfer-Encoding: base64
417
+ bodystring = Sisimai::MIME.base64d(bodystring)
418
+
419
+ elsif ctencoding == 'quoted-printable'
403
420
  # Content-Transfer-Encoding: quoted-printable
404
- bodystring = if ctencoding == 'base64'
405
- # Content-Transfer-Encoding: base64
406
- Sisimai::MIME.base64d(bodystring)
407
- else
408
- # Content-Transfer-Encoding: quoted-printable
409
- Sisimai::MIME.qprintd(bodystring)
410
- end
421
+ bodystring = Sisimai::MIME.qprintd(bodystring)
411
422
  end
412
423
 
413
424
  if mesgformat.start_with?('text/html;')
@@ -416,35 +427,10 @@ module Sisimai
416
427
  end
417
428
  else
418
429
  # NOT text/plain
419
- lowercased = bodystring.downcase
420
- if lowercased =~ ReEncoding[:'quoted-print']
421
- # Content-Transfer-Encoding: quoted-printable
422
- bodystring = Sisimai::MIME.qprintd(bodystring, mailheader)
423
- end
424
-
425
- if lowercased =~ ReEncoding[:'7bit-encoded'] &&
426
- cv = lowercased.match(ReEncoding[:'some-iso2022'])
427
- # Content-Transfer-Encoding: 7bit
428
- # Content-Type: text/plain; charset=ISO-2022-JP
429
- if ! cv[1].include?('us-ascii') && ! cv[1].include?('utf-8')
430
- bodystring = Sisimai::String.to_utf8(bodystring, cv[1])
431
- end
432
- end
433
- end
434
-
435
- # Call the hook method
436
- if hookmethod.is_a? Proc
437
- # Execute hook method
438
- begin
439
- p = {
440
- 'datasrc' => 'email',
441
- 'headers' => mailheader,
442
- 'message' => bodystring,
443
- 'bounces' => nil
444
- }
445
- havecaught = hookmethod.call(p)
446
- rescue StandardError => ce
447
- warn ' ***warning: Something is wrong in hook method :' << ce.to_s
430
+ if mesgformat.start_with?('multipart/')
431
+ # In case of Content-Type: multipart/*
432
+ p = Sisimai::MIME.makeflat(mailheader['content-type'], bodystring)
433
+ bodystring = p unless p.empty?
448
434
  end
449
435
  end
450
436
 
@@ -469,7 +455,6 @@ module Sisimai
469
455
  # 4. Sisimai::Bite::Email::*
470
456
  # 5. Sisimai::RFC3464
471
457
  # 6. Sisimai::RFC3834
472
- #
473
458
  if Sisimai::ARF.is_arf(mailheader)
474
459
  # Feedback Loop message
475
460
  scannedset = Sisimai::ARF.scan(mailheader, bodystring)
data/lib/sisimai/mime.rb CHANGED
@@ -7,11 +7,12 @@ module Sisimai
7
7
  require 'sisimai/string'
8
8
 
9
9
  ReE = {
10
- :'7bit-encoded' => %r/^content-transfer-encoding:[ ]*7bit$/m,
11
- :'quoted-print' => %r/^content-transfer-encoding:[ ]*quoted-printable$/m,
12
- :'some-iso2022' => %r/^content-type:[ ]*.+;[ ]*charset=["']?(iso-2022-[-a-z0-9]+?)['"]?$/m,
13
- :'with-charset' => %r/^content[-]type:[ ]*.+[;][ ]*charset=['"]?(.+?)['"]?$/,
14
- :'only-charset' => %r/^[\s\t]+charset=['"]?(.+?)['"]?$/,
10
+ :'7bit-encoded' => %r/^content-transfer-encoding:[ ]*7bit/m,
11
+ :'quoted-print' => %r/^content-transfer-encoding:[ ]*quoted-printable/m,
12
+ :'some-iso2022' => %r/^content-type:[ ]*.+;[ ]*charset=["']?(iso-2022-[-a-z0-9]+?)['"]?\b/m,
13
+ :'another-8bit' => %r/^content-type:[ ]*.+;[ ]*charset=["']?(.+?)['"]?\b/m,
14
+ :'with-charset' => %r/^content[-]type:[ ]*.+[;][ ]*charset=['"]?(.+?)['"]?\b/,
15
+ :'only-charset' => %r/^[\s\t]+charset=['"]?(.+?)['"]?\b/,
15
16
  :'html-message' => %r|^content-type:[ ]*text/html;|m,
16
17
  }.freeze
17
18
 
@@ -53,36 +54,30 @@ module Sisimai
53
54
  def mimedecode(argvs = [])
54
55
  characterset = nil
55
56
  encodingname = nil
56
- mimeencoded0 = nil
57
57
  decodedtext0 = []
58
- notmimetext0 = ''
59
- notmimetext1 = ''
60
58
 
61
59
  while e = argvs.shift do
62
60
  # Check and decode each element
63
61
  e = e.strip.delete('"')
64
62
 
65
63
  if self.is_mimeencoded(e)
66
- # MIME Encoded string
67
- if cv = e.match(/\A(.*)=[?]([-_0-9A-Za-z]+)[?]([BbQq])[?](.+)[?]=?(.*)\z/)
68
- # =?utf-8?B?55m954yr44Gr44KD44KT44GT?=
69
- notmimetext0 = cv[1]
70
- characterset ||= cv[2]
71
- encodingname ||= cv[3]
72
- mimeencoded0 = cv[4]
73
- notmimetext1 = cv[5]
74
-
75
- decodedtext0 << notmimetext0
76
- if encodingname == 'Q'
77
- # Quoted-Printable
78
- decodedtext0 << mimeencoded0.unpack('M').first
79
-
80
- elsif encodingname == 'B'
81
- # Base64
82
- decodedtext0 << Base64.decode64(mimeencoded0)
83
- end
84
- decodedtext0 << notmimetext1
64
+ # MIME Encoded string like "=?utf-8?B?55m954yr44Gr44KD44KT44GT?="
65
+ next unless cv = e.match(/\A(.*)=[?]([-_0-9A-Za-z]+)[?]([BbQq])[?](.+)[?]=?(.*)\z/)
66
+
67
+ characterset ||= cv[2]
68
+ encodingname ||= cv[3]
69
+ mimeencoded0 = cv[4]
70
+ decodedtext0 << cv[1]
71
+
72
+ if encodingname == 'Q'
73
+ # Quoted-Printable
74
+ decodedtext0 << mimeencoded0.unpack('M').first
75
+
76
+ elsif encodingname == 'B'
77
+ # Base64
78
+ decodedtext0 << Base64.decode64(mimeencoded0)
85
79
  end
80
+ decodedtext0 << cv[5]
86
81
  else
87
82
  decodedtext0 << e
88
83
  end
@@ -129,13 +124,10 @@ module Sisimai
129
124
  boundary01 = Sisimai::MIME.boundary(heads['content-type'], 1)
130
125
  bodystring = ''
131
126
  notdecoded = ''
132
- getencoded = ''
133
- lowercased = ''
134
127
 
135
128
  encodename = nil
136
129
  ctencoding = nil
137
130
  mimeinside = false
138
- mustencode = false
139
131
  hasdivided = argv1.split("\n")
140
132
 
141
133
  while e = hasdivided.shift do
@@ -149,8 +141,8 @@ module Sisimai
149
141
  if e == boundary00
150
142
  # The next boundary string has appeared
151
143
  # --=_gy7C4Gpes0RP4V5Bs9cK4o2Us2ZT57b-3OLnRN+4klS8dTmQ
152
- getencoded = Sisimai::String.to_utf8(notdecoded.unpack('M').first, encodename)
153
- bodystring << getencoded << e + "\n"
144
+ hasdecoded = Sisimai::String.to_utf8(notdecoded.unpack('M').first, encodename)
145
+ bodystring << hasdecoded << e + "\n"
154
146
 
155
147
  notdecoded = ''
156
148
  mimeinside = false
@@ -234,10 +226,7 @@ module Sisimai
234
226
  return nil unless argv1
235
227
 
236
228
  plain = nil
237
- if cv = argv1.match(%r|([+/\=0-9A-Za-z\r\n]+)|)
238
- # Decode BASE64
239
- plain = Base64.decode64(cv[1])
240
- end
229
+ if cv = argv1.match(%r|([+/\=0-9A-Za-z\r\n]+)|) then plain = Base64.decode64(cv[1]) end
241
230
  return plain.force_encoding('UTF-8')
242
231
  end
243
232
 
@@ -256,15 +245,202 @@ module Sisimai
256
245
  # Content-Type: multipart/report; report-type=delivery-status;
257
246
  # boundary="n6H9lKZh014511.1247824040/mx.example.jp"
258
247
  value = cv[1]
259
- value.delete!(%q|'"|)
248
+ value.delete!(%q|'";\\|)
260
249
  value = '--' + value if start > -1
261
250
  value = value + '--' if start > 0
262
251
  end
263
252
 
264
253
  return value
265
254
  end
266
- end
267
255
 
256
+ # Breaks up each multipart/* block
257
+ # @param [String] argv0 Text block of multipart/*
258
+ # @param [String] argv1 MIME type of the outside part
259
+ # @return [String] Decoded part as a plain text(text part only)
260
+ def breaksup(argv0 = nil, argv1 = '')
261
+ return nil unless argv0
262
+
263
+ hasflatten = '' # Message body including only text/plain and message/*
264
+ alsoappend = %r{\A(?:text/rfc822-headers|message/)}
265
+ thisformat = %r/\A(?:Content-Transfer-Encoding:\s*.+\n)?Content-Type:\s*([^ ;]+)/
266
+ leavesonly = %r{\A(?>
267
+ text/(?:plain|html|rfc822-headers)
268
+ |message/(?:x?delivery-status|rfc822|partial|feedback-report)
269
+ |multipart/(?:report|alternative|mixed|related|partial)
270
+ )
271
+ }x
272
+
273
+ mimeformat = '' # MIME type string of this part
274
+ alternates = argv1.start_with?('multipart/alternative') ? true : false
275
+
276
+ # Get MIME type string from Content-Type: "..." field at the first line
277
+ # or the second line of the part.
278
+ if cv = argv0.match(thisformat) then mimeformat = cv[1].downcase end
279
+
280
+ # Sisimai require only MIME types defined in $leavesonly variable
281
+ return '' unless mimeformat =~ leavesonly
282
+ return '' if alternates && mimeformat == 'text/html'
283
+
284
+ (upperchunk, lowerchunk) = argv0.split(/^$/m, 2)
285
+ upperchunk.gsub!("\n", ' ').squeeze(' ')
286
+
287
+ # Content-Description: Undelivered Message
288
+ # Content-Type: message/rfc822
289
+ # <EOM>
290
+ lowerchunk ||= ''
291
+
292
+ if mimeformat.start_with?('multipart/')
293
+ # Content-Type: multipart/*
294
+ mpboundary = Regexp.new(Regexp.escape(Sisimai::MIME.boundary(upperchunk, 0)) << "\n")
295
+ innerparts = lowerchunk.split(mpboundary)
296
+
297
+ innerparts.shift if innerparts[0].empty?
298
+ while e = innerparts.shift do
299
+ # Find internal multipart/* blocks and decode
300
+ if cv = e.match(thisformat)
301
+ # Found "Content-Type" field at the first or second line of this
302
+ # splitted part
303
+ nextformat = cv[1].downcase
304
+
305
+ next unless nextformat =~ leavesonly
306
+ next if nextformat == 'text/html'
307
+
308
+ hasflatten << Sisimai::MIME.breaksup(e, mimeformat)
309
+ else
310
+ # The content of this part is almost '--': a part of boundary
311
+ # string which is used for splitting multipart/* blocks.
312
+ hasflatten << "\n"
313
+ end
314
+ end
315
+ else
316
+ # Is not "Content-Type: multipart/*"
317
+ if cv = upperchunk.match(/Content-Transfer-Encoding: ([^\s;]+)/)
318
+ # Content-Transfer-Encoding: quoted-printable|base64|7bit|...
319
+ ctencoding = cv[1].downcase
320
+ getdecoded = ''
321
+
322
+ if ctencoding == 'quoted-printable'
323
+ # Content-Transfer-Encoding: quoted-printable
324
+ getdecoded = Sisimai::MIME.qprintd(lowerchunk)
325
+
326
+ elsif ctencoding == 'base64'
327
+ # Content-Transfer-Encoding: base64
328
+ getdecoded = Sisimai::MIME.base64d(lowerchunk)
329
+
330
+ elsif ctencoding == '7bit'
331
+ # Content-Transfer-Encoding: 7bit
332
+ if cv = upperchunk.downcase.match(ReE[:'some-iso2022'])
333
+ # Content-Type: text/plain; charset=ISO-2022-JP
334
+ getdecoded = Sisimai::String.to_utf8(lowerchunk, cv[1])
335
+ else
336
+ # No "charset" parameter in Content-Type field
337
+ getdecoded = lowerchunk
338
+ end
339
+ else
340
+ # Content-Transfer-Encoding: 8bit, binary, and so on
341
+ getdecoded = lowerchunk
342
+ end
343
+ getdecoded.gsub!(/\r\n/, "\n") # Convert CRLF to LF
344
+
345
+ if mimeformat =~ alsoappend
346
+ # Append field when the value of Content-Type: begins with
347
+ # message/ or equals text/rfc822-headers.
348
+ upperchunk.sub!(/Content-Transfer-Encoding:.+\z/, '').gsub!(/[ ]\z/, '')
349
+ hasflatten << upperchunk
350
+
351
+ elsif mimeformat == 'text/html'
352
+ # Delete HTML tags inside of text/html part whenever possible
353
+ getdecoded.gsub!(/[<][^@ ]+?[>]/, '')
354
+ end
355
+
356
+ unless getdecoded.empty?
357
+ # The string will be encoded to UTF-8 forcely and call String#scrub
358
+ # method to avoid the following errors:
359
+ # - incompatible character encodings: ASCII-8BIT and UTF-8
360
+ # - invalid byte sequence in UTF-8
361
+ unless getdecoded.encoding.to_s == 'UTF-8'
362
+ if cv = upperchunk.downcase.match(ReE[:'another-8bit'])
363
+ # ISO-8859-1, GB2312, and so on
364
+ getdecoded = Sisimai::String.to_utf8(getdecoded, cv[1])
365
+ end
366
+ end
367
+ # A part which has no "charset" parameter causes an ArgumentError:
368
+ # invalid byte sequence in UTF-8 so String#scrub should be called
369
+ hasflatten << getdecoded.scrub!('?') << "\n\n"
370
+ end
371
+ else
372
+ # Content-Type: text/plain OR text/rfc822-headers OR message/*
373
+ if mimeformat.start_with?('message/') || mimeformat == 'text/rfc822-headers'
374
+ # Append headers of multipart/* when the value of "Content-Type"
375
+ # is inlucded in the following MIME types:
376
+ # - message/delivery-status
377
+ # - message/rfc822
378
+ # - text/rfc822-headers
379
+ hasflatten << upperchunk
380
+ end
381
+ lowerchunk.sub!(/^--\z/m, '')
382
+ lowerchunk << "\n" unless lowerchunk =~ /\n\z/
383
+ hasflatten << lowerchunk
384
+ end
385
+ end
386
+
387
+ return hasflatten
388
+ end
389
+
390
+ # MIME decode entire message body
391
+ # @param [String] argv0 Content-Type header
392
+ # @param [String] argv1 Entire message body
393
+ # @return [String] Decoded message body
394
+ def makeflat(argv0 = nil, argv1 = nil)
395
+ return nil unless argv0
396
+ return nil unless argv1
397
+
398
+ ehboundary = Sisimai::MIME.boundary(argv0, 0)
399
+ mimeformat = ''
400
+ bodystring = ''
401
+
402
+ # Get MIME type string from an email header given as the 1st argument
403
+ if cv = argv0.match(%r|\A([0-9a-z]+/[^ ;]+)|) then mimeformat = cv[1] end
404
+
405
+ return '' unless mimeformat.include?('multipart/')
406
+ return '' if ehboundary.empty?
407
+
408
+ # Some bounce messages include lower-cased "content-type:" field such as
409
+ # content-type: message/delivery-status
410
+ # content-transfer-encoding: quoted-printable
411
+ argv1.gsub!(/[Cc]ontent-[Tt]ype:/m, 'Content-Type:')
412
+ argv1.gsub!(/[Cc]ontent-[Tt]ransfer-[Ee]ncodeing:/m, 'Content-Transfer-Encoding:')
413
+
414
+ # 1. Some bounce messages include upper-cased "Content-Transfer-Encoding",
415
+ # and "Content-Type" value such as
416
+ # - Content-Type: multipart/RELATED;
417
+ # - Content-Transfer-Encoding: 7BIT
418
+ # 2. Unused fields inside of mutipart/* block should be removed
419
+ argv1.gsub!(/(Content-[A-Za-z-]+?):[ ]*([^\s]+)/) do "#{$1}: #{$2.downcase}" end
420
+ argv1.gsub!(/^Content-(?:Description|Disposition):.+$/, '')
421
+
422
+ multiparts = argv1.split(Regexp.new(Regexp.escape(ehboundary) << "\n"))
423
+ multiparts.shift if multiparts[0].empty?
424
+
425
+ while e = multiparts.shift do
426
+ # Find internal multipart blocks and decode
427
+ if e =~ /\A(?:Content-[A-Za-z-]+:.+?\r\n)?Content-Type:[ ]*[^\s]+/
428
+ # Content-Type: multipart/*
429
+ bodystring << Sisimai::MIME.breaksup(e, mimeformat)
430
+ else
431
+ # Is not multipart/* block
432
+ e.sub!(%r|^Content-Transfer-Encoding:.+?\n|mi, '')
433
+ e.sub!(%r|^Content-Type:\s*text/plain.+?\n|mi, '')
434
+ bodystring << e
435
+ end
436
+ end
437
+ bodystring.gsub!(%r{^(Content-Type:\s*message/(?:rfc822|delivery-status)).+$}, '\1')
438
+ bodystring.gsub!(/^\n{2,}/, "\n")
439
+
440
+ return bodystring
441
+ end
442
+
443
+ end
268
444
  end
269
445
  end
270
446