sisimai 4.25.16 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -3
  3. data/ANALYTICAL-PRECISION +2 -2
  4. data/Benchmarks.mk +3 -3
  5. data/CONTRIBUTING +1 -1
  6. data/ChangeLog.md +412 -393
  7. data/Developers.mk +5 -6
  8. data/Gemfile +1 -1
  9. data/Makefile +15 -15
  10. data/README-JA.md +140 -78
  11. data/README.md +290 -143
  12. data/Rakefile +9 -3
  13. data/Repository.mk +2 -3
  14. data/lib/sisimai/address.rb +118 -74
  15. data/lib/sisimai/arf.rb +84 -82
  16. data/lib/sisimai/datetime.rb +5 -52
  17. data/lib/sisimai/{data → fact}/json.rb +7 -9
  18. data/lib/sisimai/fact/yaml.rb +31 -0
  19. data/lib/sisimai/fact.rb +468 -0
  20. data/lib/sisimai/lhost/activehunter.rb +12 -14
  21. data/lib/sisimai/lhost/amavis.rb +11 -14
  22. data/lib/sisimai/lhost/amazonses.rb +37 -41
  23. data/lib/sisimai/lhost/amazonworkmail.rb +15 -18
  24. data/lib/sisimai/lhost/aol.rb +12 -14
  25. data/lib/sisimai/lhost/apachejames.rb +19 -21
  26. data/lib/sisimai/lhost/barracuda.rb +10 -12
  27. data/lib/sisimai/lhost/bigfoot.rb +21 -21
  28. data/lib/sisimai/lhost/biglobe.rb +15 -16
  29. data/lib/sisimai/lhost/courier.rb +20 -20
  30. data/lib/sisimai/lhost/domino.rb +23 -19
  31. data/lib/sisimai/lhost/einsundeins.rb +23 -18
  32. data/lib/sisimai/lhost/exchange2003.rb +30 -29
  33. data/lib/sisimai/lhost/exchange2007.rb +70 -58
  34. data/lib/sisimai/lhost/exim.rb +175 -161
  35. data/lib/sisimai/lhost/ezweb.rb +31 -56
  36. data/lib/sisimai/lhost/facebook.rb +21 -33
  37. data/lib/sisimai/lhost/fml.rb +43 -48
  38. data/lib/sisimai/lhost/gmail.rb +29 -29
  39. data/lib/sisimai/lhost/gmx.rb +18 -17
  40. data/lib/sisimai/lhost/googlegroups.rb +9 -10
  41. data/lib/sisimai/lhost/gsuite.rb +21 -27
  42. data/lib/sisimai/lhost/imailserver.rb +25 -39
  43. data/lib/sisimai/lhost/interscanmss.rb +28 -31
  44. data/lib/sisimai/lhost/kddi.rb +22 -28
  45. data/lib/sisimai/lhost/mailfoundry.rb +11 -12
  46. data/lib/sisimai/lhost/mailmarshalsmtp.rb +25 -29
  47. data/lib/sisimai/lhost/mailru.rb +33 -27
  48. data/lib/sisimai/lhost/mcafee.rb +21 -31
  49. data/lib/sisimai/lhost/messagelabs.rb +17 -20
  50. data/lib/sisimai/lhost/messagingserver.rb +40 -37
  51. data/lib/sisimai/lhost/mfilter.rb +15 -16
  52. data/lib/sisimai/lhost/mxlogic.rb +24 -23
  53. data/lib/sisimai/lhost/notes.rb +17 -17
  54. data/lib/sisimai/lhost/office365.rb +63 -27
  55. data/lib/sisimai/lhost/opensmtpd.rb +12 -13
  56. data/lib/sisimai/lhost/outlook.rb +12 -15
  57. data/lib/sisimai/lhost/postfix.rb +179 -129
  58. data/lib/sisimai/lhost/powermta.rb +12 -14
  59. data/lib/sisimai/lhost/qmail.rb +44 -47
  60. data/lib/sisimai/lhost/receivingses.rb +15 -20
  61. data/lib/sisimai/lhost/sendgrid.rb +34 -32
  62. data/lib/sisimai/lhost/sendmail.rb +66 -53
  63. data/lib/sisimai/lhost/surfcontrol.rb +19 -19
  64. data/lib/sisimai/lhost/v5sendmail.rb +45 -39
  65. data/lib/sisimai/lhost/verizon.rb +35 -39
  66. data/lib/sisimai/lhost/x1.rb +18 -17
  67. data/lib/sisimai/lhost/x2.rb +17 -14
  68. data/lib/sisimai/lhost/x3.rb +19 -19
  69. data/lib/sisimai/lhost/x4.rb +72 -57
  70. data/lib/sisimai/lhost/x5.rb +17 -19
  71. data/lib/sisimai/lhost/x6.rb +41 -17
  72. data/lib/sisimai/lhost/yahoo.rb +17 -16
  73. data/lib/sisimai/lhost/yandex.rb +16 -20
  74. data/lib/sisimai/lhost/zoho.rb +16 -15
  75. data/lib/sisimai/lhost.rb +8 -10
  76. data/lib/sisimai/mail/maildir.rb +1 -3
  77. data/lib/sisimai/mail/mbox.rb +3 -4
  78. data/lib/sisimai/mail/memory.rb +0 -1
  79. data/lib/sisimai/mail/stdin.rb +1 -3
  80. data/lib/sisimai/mail.rb +3 -7
  81. data/lib/sisimai/mda.rb +28 -42
  82. data/lib/sisimai/message.rb +435 -326
  83. data/lib/sisimai/order.rb +5 -5
  84. data/lib/sisimai/reason/authfailure.rb +64 -0
  85. data/lib/sisimai/reason/badreputation.rb +53 -0
  86. data/lib/sisimai/reason/blocked.rb +94 -160
  87. data/lib/sisimai/reason/contenterror.rb +8 -9
  88. data/lib/sisimai/reason/delivered.rb +4 -6
  89. data/lib/sisimai/reason/exceedlimit.rb +10 -12
  90. data/lib/sisimai/reason/expired.rb +6 -8
  91. data/lib/sisimai/reason/feedback.rb +2 -3
  92. data/lib/sisimai/reason/filtered.rb +17 -19
  93. data/lib/sisimai/reason/hasmoved.rb +9 -10
  94. data/lib/sisimai/reason/hostunknown.rb +15 -15
  95. data/lib/sisimai/reason/mailboxfull.rb +10 -12
  96. data/lib/sisimai/reason/mailererror.rb +18 -20
  97. data/lib/sisimai/reason/mesgtoobig.rb +9 -11
  98. data/lib/sisimai/reason/networkerror.rb +5 -8
  99. data/lib/sisimai/reason/norelaying.rb +8 -11
  100. data/lib/sisimai/reason/notaccept.rb +13 -14
  101. data/lib/sisimai/reason/notcompliantrfc.rb +43 -0
  102. data/lib/sisimai/reason/onhold.rb +6 -9
  103. data/lib/sisimai/reason/policyviolation.rb +14 -12
  104. data/lib/sisimai/reason/rejected.rb +26 -24
  105. data/lib/sisimai/reason/requireptr.rb +69 -0
  106. data/lib/sisimai/reason/securityerror.rb +33 -36
  107. data/lib/sisimai/reason/spamdetected.rb +114 -147
  108. data/lib/sisimai/reason/speeding.rb +49 -0
  109. data/lib/sisimai/reason/suspend.rb +11 -11
  110. data/lib/sisimai/reason/syntaxerror.rb +11 -10
  111. data/lib/sisimai/reason/systemerror.rb +7 -9
  112. data/lib/sisimai/reason/systemfull.rb +7 -8
  113. data/lib/sisimai/reason/toomanyconn.rb +9 -11
  114. data/lib/sisimai/reason/undefined.rb +2 -3
  115. data/lib/sisimai/reason/userunknown.rb +129 -146
  116. data/lib/sisimai/reason/vacation.rb +3 -4
  117. data/lib/sisimai/reason/virusdetected.rb +10 -11
  118. data/lib/sisimai/reason.rb +59 -64
  119. data/lib/sisimai/rfc1894.rb +55 -28
  120. data/lib/sisimai/rfc2045.rb +373 -0
  121. data/lib/sisimai/rfc3464.rb +250 -308
  122. data/lib/sisimai/rfc3834.rb +42 -45
  123. data/lib/sisimai/rfc5322.rb +75 -100
  124. data/lib/sisimai/rfc5965.rb +31 -0
  125. data/lib/sisimai/rhost/cox.rb +5 -6
  126. data/lib/sisimai/rhost/franceptt.rb +6 -8
  127. data/lib/sisimai/rhost/godaddy.rb +12 -12
  128. data/lib/sisimai/rhost/{googleapps.rb → google.rb} +80 -72
  129. data/lib/sisimai/rhost/iua.rb +9 -10
  130. data/lib/sisimai/rhost/kddi.rb +6 -8
  131. data/lib/sisimai/rhost/{exchangeonline.rb → microsoft.rb} +115 -114
  132. data/lib/sisimai/rhost/mimecast.rb +42 -40
  133. data/lib/sisimai/rhost/nttdocomo.rb +12 -12
  134. data/lib/sisimai/rhost/spectrum.rb +10 -12
  135. data/lib/sisimai/rhost/{tencentqq.rb → tencent.rb} +7 -8
  136. data/lib/sisimai/rhost.rb +23 -31
  137. data/lib/sisimai/smtp/command.rb +59 -0
  138. data/lib/sisimai/smtp/error.rb +4 -7
  139. data/lib/sisimai/smtp/reply.rb +161 -74
  140. data/lib/sisimai/smtp/status.rb +504 -393
  141. data/lib/sisimai/smtp/transcript.rb +124 -0
  142. data/lib/sisimai/smtp.rb +0 -1
  143. data/lib/sisimai/string.rb +74 -5
  144. data/lib/sisimai/time.rb +1 -2
  145. data/lib/sisimai/version.rb +1 -1
  146. data/lib/sisimai.rb +35 -21
  147. data/set-of-emails/maildir/bsd/lhost-domino-02.eml +6 -3
  148. data/set-of-emails/maildir/bsd/lhost-googlegroups-15.eml +174 -0
  149. data/set-of-emails/maildir/bsd/lhost-gsuite-15.eml +229 -0
  150. data/set-of-emails/maildir/bsd/lhost-postfix-75.eml +51 -0
  151. data/set-of-emails/maildir/bsd/lhost-postfix-76.eml +101 -0
  152. data/set-of-emails/maildir/bsd/lhost-postfix-77.eml +74 -0
  153. data/set-of-emails/maildir/bsd/lhost-postfix-78.eml +91 -0
  154. data/set-of-emails/maildir/bsd/lhost-receivingses-08.eml +88 -0
  155. data/set-of-emails/maildir/bsd/rfc3464-43.eml +88 -0
  156. data/set-of-emails/maildir/bsd/rhost-google-03.eml +101 -0
  157. data/set-of-emails/maildir/bsd/rhost-google-04.eml +102 -0
  158. data/set-of-emails/maildir/bsd/rhost-google-05.eml +82 -0
  159. data/set-of-emails/maildir/bsd/rhost-google-06.eml +102 -0
  160. data/set-of-emails/maildir/bsd/rhost-google-07.eml +69 -0
  161. data/set-of-emails/maildir/bsd/rhost-google-08.eml +99 -0
  162. data/sisimai-java.gemspec +1 -1
  163. data/sisimai.gemspec +1 -1
  164. metadata +41 -20
  165. data/.rspec +0 -2
  166. data/lib/sisimai/data/yaml.rb +0 -33
  167. data/lib/sisimai/data.rb +0 -411
  168. data/lib/sisimai/mime.rb +0 -456
  169. /data/set-of-emails/maildir/bsd/{rfc3464-41.eml → rfc3834-05.eml} +0 -0
  170. /data/set-of-emails/maildir/bsd/{rhost-googleapps-01.eml → rhost-google-01.eml} +0 -0
  171. /data/set-of-emails/maildir/bsd/{rhost-googleapps-02.eml → rhost-google-02.eml} +0 -0
  172. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-01.eml → rhost-microsoft-01.eml} +0 -0
  173. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-02.eml → rhost-microsoft-02.eml} +0 -0
  174. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-03.eml → rhost-microsoft-03.eml} +0 -0
  175. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-01.eml → rhost-tencent-01.eml} +0 -0
  176. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-02.eml → rhost-tencent-02.eml} +0 -0
  177. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-03.eml → rhost-tencent-03.eml} +0 -0
@@ -0,0 +1,373 @@
1
+ module Sisimai
2
+ # Sisimai::RFC2045 is a MIME Utilities for Sisimai.
3
+ module RFC2045
4
+ class << self
5
+ require 'base64'
6
+ require 'sisimai/string'
7
+
8
+ # Check that the argument is MIME-Encoded string or not
9
+ # @param [String] argvs String to be checked
10
+ # @return [True,False] false: Not MIME encoded string
11
+ # true: MIME encoded string
12
+ def is_encoded(argv1)
13
+ return nil unless argv1
14
+
15
+ text1 = argv1.delete('"')
16
+ mime1 = false
17
+ piece = []
18
+
19
+ if text1.include?(' ')
20
+ # Multiple MIME-Encoded strings in a line
21
+ piece = text1.split(' ')
22
+ else
23
+ piece << text1
24
+ end
25
+
26
+ while e = piece.shift do
27
+ # Check all the string in the array
28
+ next unless e =~ /[ \t]*=[?][-_0-9A-Za-z]+[?][BbQq][?].+[?]=?[ \t]*/
29
+ mime1 = true
30
+ end
31
+ return mime1
32
+ end
33
+
34
+ # Decode MIME-Encoded string in an email header
35
+ # @param [Array] argvs An array including MIME-Encoded text
36
+ # @return [String] MIME-Decoded text
37
+ def decodeH(argvs = [])
38
+ ctxcharset = nil
39
+ qbencoding = nil
40
+ textblocks = []
41
+
42
+ while e = argvs.shift do
43
+ # Check and decode each element
44
+ e = e.strip.delete('"')
45
+
46
+ if self.is_encoded(e)
47
+ # MIME Encoded string like "=?utf-8?B?55m954yr44Gr44KD44KT44GT?="
48
+ next unless cv = e.match(/\A(.*)=[?]([-_0-9A-Za-z]+)[?]([BbQq])[?](.+)[?]=?(.*)\z/)
49
+
50
+ ctxcharset ||= cv[2]
51
+ qbencoding ||= cv[3]
52
+ notdecoded = cv[4]
53
+
54
+ textblocks << cv[1]
55
+ textblocks << if qbencoding.upcase == 'B'
56
+ Base64.decode64(notdecoded)
57
+ else
58
+ notdecoded.unpack('M').first
59
+ end
60
+ textblocks[-1].gsub!(/\r\n/, '')
61
+ textblocks << cv[5]
62
+ else
63
+ textblocks << if textblocks.empty? then e else ' ' << e end
64
+ end
65
+ end
66
+
67
+ return '' if textblocks.empty?
68
+ p = textblocks.join('')
69
+
70
+ if ctxcharset && qbencoding
71
+ # utf8 => UTF-8
72
+ ctxcharset = 'UTF-8' if ctxcharset.casecmp('UTF8') == 0
73
+
74
+ unless ctxcharset.casecmp('UTF-8') == 0
75
+ # Characterset is not UTF-8
76
+ begin
77
+ p .encode!('UTF-8', ctxcharset)
78
+ rescue
79
+ p = 'FAILED TO CONVERT THE SUBJECT'
80
+ end
81
+ end
82
+ end
83
+
84
+ return p.force_encoding('UTF-8').scrub('?')
85
+ end
86
+
87
+ # Decode MIME BASE64 Encoded string
88
+ # @param [String] argv0 MIME Encoded text
89
+ # @return [String] MIME-Decoded text
90
+ def decodeB(argv0 = nil)
91
+ return nil unless argv0
92
+
93
+ p = nil
94
+ if cv = argv0.match(%r|([+/\=0-9A-Za-z\r\n]+)|) then p = Base64.decode64(cv[1]) end
95
+ return p ? p.scrub('?') : nil
96
+ end
97
+
98
+ # Decode MIME Quoted-Printable Encoded string
99
+ # @param [String] argv0 MIME Encoded text
100
+ # @return [String] MIME Decoded text
101
+ def decodeQ(argv0 = nil)
102
+ return nil unless argv0
103
+ return argv0.unpack('M').first.scrub('?')
104
+ end
105
+
106
+ # Find a value of specified field name from Content-Type: header
107
+ # @param [String] argv0 The value of Content-Type: header
108
+ # @param [String] argv1 Lower-cased attribute name of the parameter
109
+ # @return [String] The value of the parameter
110
+ # @since v5.0.0
111
+ def parameter(argv0 = '', argv1 = '')
112
+ return nil if argv0.empty?
113
+ parameterq = argv1.size > 0 ? argv1 + '=' : ''
114
+ paramindex = argv1.size > 0 ? argv0.index(parameterq) : 0
115
+ return '' unless paramindex
116
+
117
+ # Find the value of the parameter name specified in argv1
118
+ foundtoken = argv0[paramindex + parameterq.size, argv0.size].split(';', 2)[0] || ''
119
+ foundtoken = foundtoken.downcase unless argv1 == 'boundary'
120
+ foundtoken = foundtoken.delete('"').delete("'")
121
+ return foundtoken
122
+ end
123
+
124
+ # Get a boundary string
125
+ # @param [String] argv0 The value of Content-Type header
126
+ # @param [Integer] start -1: boundary string itself
127
+ # 0: Start of boundary
128
+ # 1: End of boundary
129
+ # @return [String] Boundary string
130
+ def boundary(argv0 = '', start = -1)
131
+ return nil if argv0.empty?
132
+ btext = parameter(argv0, 'boundary')
133
+ return '' if btext.empty?
134
+
135
+ # Content-Type: multipart/mixed; boundary=Apple-Mail-5--931376066
136
+ # Content-Type: multipart/report; report-type=delivery-status;
137
+ # boundary="n6H9lKZh014511.1247824040/mx.example.jp"
138
+ btext = '--' + btext if start > -1
139
+ btext = btext + '--' if start > 0
140
+ return btext
141
+ end
142
+
143
+ # Cut header fields except Content-Type, Content-Transfer-Encoding from multipart/* block
144
+ # @param [String] block multipart/* block text
145
+ # @param [Boolean] heads true = Returns only Content-(Type|Transfer-Encoding) headers
146
+ # @return [Array] Two headers and body part of multipart/* block
147
+ # @since v5.0.0
148
+ def haircut(block = '', heads = false)
149
+ return nil if block.empty?
150
+
151
+ (upperchunk, lowerchunk) = block.split("\n\n", 2)
152
+ return ['', ''] if upperchunk.to_s.empty?
153
+ return ['', ''] unless upperchunk.index('Content-Type')
154
+
155
+ headerpart = ['', ''] # ["text/plain; charset=iso-2022-jp; ...", "quoted-printable"]
156
+ multipart1 = [] # [headerpart, "body"]
157
+
158
+ upperchunk.split("\n").each do |e|
159
+ # Remove fields except Content-Type:, and Content-Transfer-Encoding: in each part of
160
+ # multipart/* block such as the following:
161
+ # Date: Thu, 29 Apr 2018 22:22:22 +0900
162
+ # MIME-Version: 1.0
163
+ # Message-ID: ...
164
+ # Content-Transfer-Encoding: quoted-printable
165
+ # Content-Type: text/plain; charset=us-ascii
166
+ if e.index('Content-Type:') == 0
167
+ # Content-Type: ***
168
+ v = e.split(' ', 2)[-1]
169
+ headerpart[0] = v.index('boundary=') ? v : v.downcase
170
+
171
+ elsif e.index('Content-Transfer-Encoding:') == 0
172
+ # Content-Transfer-Encoding: ***
173
+ headerpart[1] = e.split(' ', 2)[-1].downcase
174
+
175
+ elsif e.index('boundary=') || e.index('charset=')
176
+ # "Content-Type" field has boundary="..." or charset="utf-8"
177
+ next if headerpart[0].empty?
178
+ headerpart[0] << " " << e
179
+ headerpart[0].gsub!(/\s\s+/, ' ')
180
+ end
181
+ end
182
+ return headerpart if heads
183
+
184
+ mediatypev = headerpart[0].downcase
185
+ ctencoding = headerpart[1]
186
+ multipart1 = headerpart << ''
187
+
188
+ while true do
189
+ # Check the upper block: Make a body part at the 2nd element of multipart1
190
+ multipart1[2] = sprintf("Content-Type: %s\n", headerpart[0])
191
+
192
+ # Do not append Content-Transfer-Encoding: header when the part is the original message:
193
+ # Content-Type is message/rfc822 or text/rfc822-headers, or message/delivery-status, or
194
+ # message/feedback-report
195
+ break if mediatypev.index('/rfc822')
196
+ break if mediatypev.index('/delivery-status')
197
+ break if mediatypev.index('/feedback-report')
198
+ break if ctencoding.empty?
199
+
200
+ multipart1[2] << sprintf("Content-Transfer-Encoding: %s\n", ctencoding)
201
+ break
202
+ end
203
+
204
+ while true do
205
+ # Append LF before the lower chunk into the 2nd element of multipart1
206
+ break if lowerchunk.empty?
207
+ break if lowerchunk[0, 1] == "\n"
208
+
209
+ multipart1[2] << "\n"
210
+ break
211
+ end
212
+ multipart1[2] << lowerchunk
213
+ return multipart1
214
+ end
215
+
216
+ # Split argv1: multipart/* blocks by a boundary string in argv0
217
+ # @param [String] argv0 The value of Content-Type header
218
+ # @param [String] argv1 A pointer to multipart/* message blocks
219
+ # @return [Array] List of each part of multipart/*
220
+ # @since v5.0.0
221
+ def levelout(argv0 = '', argv1 = '')
222
+ return [] if argv0.empty?
223
+ return [] if argv1.empty?
224
+
225
+ boundary01 = boundary(argv0, 0); return [] if boundary01.empty?
226
+ multiparts = argv1.split(Regexp.new(Regexp.escape(boundary01) + "\n"))
227
+ partstable = []
228
+
229
+ multiparts.shift if multiparts[0].size < 8
230
+ multiparts.pop if multiparts[-1].size < 8
231
+
232
+ while e = multiparts.shift do
233
+ # Check each part and breaks up internal multipart/* block
234
+ f = haircut(e)
235
+ if f[0].index('multipart/')
236
+ # There is nested multipart/* block
237
+ boundary02 = boundary(f[0], -1); next if boundary02.empty?
238
+ bodyinside = f[-1].split("\n\n", 2)[-1]
239
+ next unless bodyinside.size > 8
240
+ next unless bodyinside.index(boundary02)
241
+
242
+ v = levelout(f[0], bodyinside)
243
+ partstable += v if v.size > 0
244
+ else
245
+ # The part is not a multipart/* block
246
+ b = f[-1].size > 0 ? f[-1] : e
247
+ v = [f[0], f[1], f[0].size > 0 ? b.split("\n\n", 2)[-1] : b]
248
+ partstable << v
249
+ end
250
+ end
251
+ return [] if partstable.empty?
252
+
253
+ # Remove $boundary01.'--' and strings from the boundary to the end of the body part.
254
+ boundary01.chomp!
255
+ b = partstable[-1][2]
256
+ p = b.index(boundary01 + '--')
257
+ b[p, b.size] = "" if p
258
+
259
+ return partstable
260
+ end
261
+
262
+ # Make flat multipart/* part blocks and decode
263
+ # @param [String] argv0 The value of Content-Type header
264
+ # @param [String] argv1 A pointer to multipart/* message blocks
265
+ # @return [String] Message body
266
+ def makeflat(argv0 = '', argv1 = '')
267
+ return nil unless argv0
268
+ return nil unless argv1
269
+ return '' unless argv0.index('multipart/')
270
+ return '' unless argv0.index('boundary=')
271
+
272
+ # Some bounce messages include lower-cased "content-type:" field such as the followings:
273
+ # - content-type: message/delivery-status => Content-Type: message/delivery-status
274
+ # - content-transfer-encoding: quoted-printable => Content-Transfer-Encoding: quoted-printable
275
+ # - CHARSET=, BOUNDARY= => charset-, boundary=
276
+ # - message/xdelivery-status => message/delivery-status
277
+ iso2022set = %r/charset=["']?(iso-2022-[-a-z0-9]+)['"]?\b/
278
+ multiparts = levelout(argv0, argv1)
279
+ flattenout = ''
280
+
281
+ while e = multiparts.shift do
282
+ # Pick only the following parts Sisimai::Lhost will use, and decode each part
283
+ # - text/plain, text/rfc822-headers
284
+ # - message/delivery-status, message/rfc822, message/partial, message/feedback-report
285
+ istexthtml = false
286
+ mediatypev = parameter(e[0]) || 'text/plain';
287
+ next if mediatypev.start_with?('text/', 'message/') == false
288
+
289
+ if mediatypev == 'text/html'
290
+ # Skip text/html part when the value of Content-Type: header in an internal part of
291
+ # multipart/* includes multipart/alternative;
292
+ next if argv0.index('multipart/alternative')
293
+ istexthtml = true
294
+ end
295
+
296
+ ctencoding = e[1]
297
+ bodyinside = e[2]
298
+ bodystring = ''
299
+
300
+ if ctencoding.size > 0
301
+ # Check the value of Content-Transfer-Encoding: header
302
+ if ctencoding == 'base64'
303
+ # Content-Transfer-Encoding: base64
304
+ bodystring = decodeB(bodyinside) || ''
305
+
306
+ elsif ctencoding == 'quoted-printable'
307
+ # Content-Transfer-Encoding: quoted-printable
308
+ bodystring = decodeQ(bodyinside) || ''
309
+
310
+ elsif ctencoding == '7bit'
311
+ # Content-Transfer-Encoding: 7bit
312
+ if cv = e[0].downcase.match(iso2022set)
313
+ # Content-Type: text/plain; charset=ISO-2022-JP
314
+ bodystring = Sisimai::String.to_utf8(bodyinside, cv[1]) || ''
315
+ else
316
+ # No "charset" parameter in the value of Content-Type: header
317
+ bodystring = bodyinside
318
+ end
319
+ else
320
+ # Content-Transfer-Encoding: 8bit, binary, and so on
321
+ bodystring = bodyinside
322
+ end
323
+
324
+ if istexthtml
325
+ # Try to delete HTML tags inside of text/html part whenever possible
326
+ bodystring = Sisimai::String.to_plain(bodystring) || ''
327
+ end
328
+ next if bodystring.empty?
329
+
330
+ # The body string will be encoded to UTF-8 forcely and call String#scrub method to avoid
331
+ # the following errors:
332
+ # - incompatible character encodings: ASCII-8BIT and UTF-8
333
+ # - invalid byte sequence in UTF-8
334
+ unless bodystring.encoding.to_s == 'UTF-8'
335
+ # ASCII-8BIT or other 8bit encodings
336
+ ctxcharset = parameter(e[0], 'charset')
337
+ if ctxcharset.empty?
338
+ # The part which has no "charset" parameter causes an ArgumentError: invalid byte
339
+ # sequence in UTF-8 so String#scrub should be called
340
+ bodystring.scrub!('?')
341
+ else
342
+ # ISO-8859-1, GB2312, and so on
343
+ bodystring = Sisimai::String.to_utf8(bodystring, ctxcharset) || ''
344
+ end
345
+ bodystring << "\n\n"
346
+ end
347
+
348
+ bodystring.gsub!(/\r\n/, "\n") if bodystring.include?("\r\n") # Convert CRLF to LF
349
+
350
+ else
351
+ # There is no Content-Transfer-Encoding header in the part
352
+ bodystring << bodyinside
353
+ end
354
+
355
+ if mediatypev.include?('/delivery-status') || mediatypev.include?('/feedback-report') || mediatypev.include?('/rfc822')
356
+ # Add Content-Type: header of each part (will be used as a delimiter at Sisimai::Lhost)
357
+ # into the body inside when the value of Content-Type: field is message/delivery-status,
358
+ # message/rfc822, or text/rfc822-headers
359
+ bodystring = sprintf("Content-Type: %s\n%s", mediatypev, bodystring)
360
+ end
361
+
362
+ # Append "\n" when the last character of $bodystring is not LF
363
+ bodystring << "\n\n" unless bodystring[-2, 2] == "\n\n"
364
+ flattenout << bodystring
365
+ end
366
+
367
+ return flattenout
368
+ end
369
+
370
+ end
371
+ end
372
+ end
373
+