sisimai 4.25.17 → 5.0.0

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 (178) 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 +406 -407
  7. data/Developers.mk +5 -6
  8. data/Gemfile +1 -1
  9. data/Makefile +12 -12
  10. data/README-JA.md +142 -94
  11. data/README.md +282 -150
  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 +20 -16
  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 -325
  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 +13 -18
  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 -21
  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/mac/reported-from-nick4tech-san-01.eml +0 -6
  170. /data/set-of-emails/maildir/bsd/{rfc3464-41.eml → rfc3834-05.eml} +0 -0
  171. /data/set-of-emails/maildir/bsd/{rhost-googleapps-01.eml → rhost-google-01.eml} +0 -0
  172. /data/set-of-emails/maildir/bsd/{rhost-googleapps-02.eml → rhost-google-02.eml} +0 -0
  173. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-01.eml → rhost-microsoft-01.eml} +0 -0
  174. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-02.eml → rhost-microsoft-02.eml} +0 -0
  175. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-03.eml → rhost-microsoft-03.eml} +0 -0
  176. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-01.eml → rhost-tencent-01.eml} +0 -0
  177. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-02.eml → rhost-tencent-02.eml} +0 -0
  178. /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
+