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
@@ -1,7 +1,6 @@
1
1
  module Sisimai
2
2
  # Sisimai::DateTime provide methods for dealing date and time.
3
3
  module DateTime
4
- # Imported from p5-Sisimail/lib/Sisimai/DateTime.pm
5
4
  require 'date'
6
5
 
7
6
  class << self
@@ -172,40 +171,6 @@ module Sisimai
172
171
  #'YEKT' => '+0500', # Yekaterinburg Time UTC+05:00
173
172
  }.freeze
174
173
 
175
- # Convert to second
176
- # @param [String] argv1 Digit and a unit of time
177
- # @return [Integer] n: seconds
178
- # 0: 0 or invalid unit of time
179
- # @example Get the value of seconds
180
- # to_second('1d') #=> 86400
181
- # to_second('2h') #=> 7200
182
- def to_second(argv1)
183
- return 0 unless argv1.is_a?(::String)
184
-
185
- getseconds = 0
186
- unitoftime = TimeUnit.keys.join
187
- mathconsts = MathematicalConstant.keys.join
188
-
189
- if cr = argv1.match(/\A(\d+|\d+[.]\d+)([#{unitoftime}])?\z/)
190
- # 1d, 1.5w
191
- n = cr[1].to_f
192
- u = cr[2] || 'd'
193
- getseconds = n * TimeUnit[u].to_f
194
-
195
- elsif cr = argv1.match(/\A(\d+|\d+[.]\d+)?([#{mathconsts}])([#{unitoftime}])?\z/)
196
- # 1pd, 1.5pw
197
- n = cr[1].to_f || 1
198
- n = 1 if n.to_i == 0
199
- m = MathematicalConstant[cr[2]].to_f
200
- u = cr[3] || 'd'
201
- getseconds = n * m * TimeUnit[u].to_f
202
- else
203
- getseconds = 0
204
- end
205
-
206
- return getseconds
207
- end
208
-
209
174
  # Month name list
210
175
  # @param [Boolean] argv1 Require full name or not
211
176
  # @return [Array, String] Month name list or month name
@@ -217,17 +182,6 @@ module Sisimai
217
182
  return MonthName[value]
218
183
  end
219
184
 
220
- # List of day of week
221
- # @param [Boolean] argv1 Require full name
222
- # @return [Array, String] List of day of week or day of week
223
- # @example Get the names of each day of week
224
- # dayofweek() #=> [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
225
- # dayofweek(true) #=> [ 'Sunday', 'Monday', 'Tuesday', ... ]
226
- def dayofweek(argv1 = false)
227
- value = argv1 ? :full : :abbr
228
- return DayOfWeek[value]
229
- end
230
-
231
185
  # Parse date string; strptime() wrapper
232
186
  # @param [String] argv1 Date string
233
187
  # @return [String] Converted date string
@@ -259,7 +213,7 @@ module Sisimai
259
213
  # Parse each piece of time
260
214
  if p =~ /\A[A-Z][a-z]{2,}[,]?\z/
261
215
  # Day of week or Day of week; Thu, Apr, ...
262
- p.gsub!(/,\z/, '') if p.end_with?(',') # "Thu," => "Thu"
216
+ p[-1, 1] = '' if p.end_with?(',') # "Thu," => "Thu"
263
217
  p = p[0,3] if p.size > 3
264
218
 
265
219
  if DayOfWeek[:abbr].include?(p)
@@ -302,7 +256,7 @@ module Sisimai
302
256
  # Time: 1:4 => 01:04:00
303
257
  v[:T] = sprintf('%02d:%02d:00', cr[1].to_i, cr[2].to_i)
304
258
 
305
- elsif p =~ /\A[APap][Mm]\z/
259
+ elsif p.downcase == 'am' || p.downcase == 'pm'
306
260
  # AM or PM
307
261
  afternoon1 = 1
308
262
  else
@@ -394,8 +348,8 @@ module Sisimai
394
348
 
395
349
  # Abbreviation -> Tiemzone
396
350
  # @param [String] argv1 Abbr. e.g.) JST, GMT, PDT
397
- # @return [String, Nil] +0900, +0000, -0600 or nil if the argument is
398
- # invalid format or not supported abbreviation
351
+ # @return [String, Nil] +0900, +0000, -0600 or nil if the argument is invalid format or
352
+ # not supported abbreviation
399
353
  # @example Get the timezone string of "JST"
400
354
  # abbr2tz('JST') #=> '+0900'
401
355
  def abbr2tz(argv1)
@@ -405,8 +359,7 @@ module Sisimai
405
359
 
406
360
  # Convert to second
407
361
  # @param [String] argv1 Timezone string e.g) +0900
408
- # @return [Integer, Nil] n: seconds or nil it the argument is invalid
409
- # format string
362
+ # @return [Integer, Nil] n: seconds or nil it the argument is invalid format string
410
363
  # @see second2tz
411
364
  # @example Convert '+0900' to seconds
412
365
  # tz2second('+0900') #=> 32400
@@ -1,17 +1,15 @@
1
1
  module Sisimai
2
- class Data
3
- # Sisimai::Data::JSON dumps parsed data object as a JSON format. This class
4
- # and method should be called from the parent object "Sisimai::Data".
2
+ class Fact
3
+ # Sisimai::Fact::JSON dumps parsed data object as a JSON format. This class and method should be
4
+ # called from the parent object "Sisimai::Fact".
5
5
  module JSON
6
- # Imported from p5-Sisimail/lib/Sisimai/Data/JSON.pm
7
6
  class << self
8
- # Data dumper(JSON)
9
- # @param [Sisimai::Data] argvs Object
10
- # @return [String, Nil] Dumped data or nil if the argument
11
- # is missing
7
+ # Serializer (JSON)
8
+ # @param [Sisimai::Fact] argvs Object
9
+ # @return [String, nil] Dumped data or nil if the argument is missing
12
10
  def dump(argvs)
13
11
  return nil unless argvs
14
- return nil unless argvs.is_a? Sisimai::Data
12
+ return nil unless argvs.is_a? Sisimai::Fact
15
13
 
16
14
  if RUBY_PLATFORM.start_with?('java')
17
15
  # java-based ruby environment like JRuby.
@@ -0,0 +1,31 @@
1
+ module Sisimai
2
+ class Fact
3
+ # Sisimai::Fact::YAML dumps parsed data object as a YAML format. This class and method should be
4
+ # called from the parent object "Sisimai::Fact".
5
+ module YAML
6
+ class << self
7
+ require 'yaml'
8
+
9
+ # Serializer (YAML)
10
+ # @param [Sisimai::Fact] argvs Object
11
+ # @return [String, nil] Dumped data or nil if the argument is missing
12
+ def dump(argvs)
13
+ return nil unless argvs
14
+ return nil unless argvs.is_a? Sisimai::Fact
15
+
16
+ damneddata = argvs.damn
17
+ yamlstring = nil
18
+
19
+ begin
20
+ yamlstring = ::YAML.dump(damneddata)
21
+ rescue StandardError => ce
22
+ warn '***warning: Failed to YAML.dump: ' << ce.to_s
23
+ end
24
+
25
+ return yamlstring
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,468 @@
1
+ module Sisimai
2
+ # Sisimai::Fact generate parsed data
3
+ class Fact
4
+ require 'sisimai/message'
5
+ require 'sisimai/rfc1894'
6
+ require 'sisimai/rfc5322'
7
+ require 'sisimai/reason'
8
+ require 'sisimai/address'
9
+ require 'sisimai/datetime'
10
+ require 'sisimai/time'
11
+ require 'sisimai/smtp/error'
12
+ require 'sisimai/smtp/command'
13
+ require 'sisimai/string'
14
+ require 'sisimai/rhost'
15
+
16
+ @@rwaccessors = [
17
+ :action, # [String] The value of Action: header
18
+ :addresser, # [Sisimai::Address] From address
19
+ :alias, # [String] Alias of the recipient address
20
+ :catch, # [?] Results generated by hook method
21
+ :deliverystatus, # [String] Delivery Status(DSN)
22
+ :destination, # [String] The domain part of the "recipinet"
23
+ :diagnosticcode, # [String] Diagnostic-Code: Header
24
+ :diagnostictype, # [String] The 1st part of Diagnostic-Code: Header
25
+ :feedbacktype, # [String] Feedback Type
26
+ :hardbounce, # [Boolean] true = Hard bounce, false = is not a hard bounce
27
+ :lhost, # [String] local host name/Local MTA
28
+ :listid, # [String] List-Id header of each ML
29
+ :messageid, # [String] Message-Id: header
30
+ :origin, # [String] Email path as a data source
31
+ :reason, # [String] Bounce reason
32
+ :recipient, # [Sisimai::Address] Recipient address which bounced
33
+ :replycode, # [String] SMTP Reply Code
34
+ :rhost, # [String] Remote host name/Remote MTA
35
+ :senderdomain, # [String] The domain part of the "addresser"
36
+ :smtpagent, # [String] Module(Engine) name
37
+ :smtpcommand, # [String] The last SMTP command
38
+ :subject, # [String] UTF-8 Subject text
39
+ :timestamp, # [Sisimai::Time] Date: header in the original message
40
+ :timezoneoffset, # [Integer] Time zone offset(seconds)
41
+ :token, # [String] Message token/MD5 Hex digest value
42
+ ]
43
+ attr_accessor(*@@rwaccessors)
44
+
45
+ RetryIndex = Sisimai::Reason.retry
46
+ RFC822Head = Sisimai::RFC5322.HEADERFIELDS(:all)
47
+ ActionList = { delayed: 1, delivered: 1, expanded: 1, failed: 1, relayed: 1 };
48
+
49
+ # Constructor of Sisimai::Fact
50
+ # @param [Hash] argvs Including each parameter
51
+ # @return [Sisimai::Fact] Structured email data
52
+ def initialize(argvs)
53
+ # Create email address object
54
+ @alias = argvs['alias'] || ''
55
+ @addresser = argvs['addresser']
56
+ @action = argvs['action']
57
+ @catch = argvs['catch']
58
+ @diagnosticcode = argvs['diagnosticcode']
59
+ @diagnostictype = argvs['diagnostictype']
60
+ @deliverystatus = argvs['deliverystatus']
61
+ @destination = argvs['recipient'].host
62
+ @feedbacktype = argvs['feedbacktype']
63
+ @hardbounce = argvs['hardbounce']
64
+ @lhost = argvs['lhost']
65
+ @listid = argvs['listid']
66
+ @messageid = argvs['messageid']
67
+ @origin = argvs['origin']
68
+ @reason = argvs['reason']
69
+ @recipient = argvs['recipient']
70
+ @replycode = argvs['replycode']
71
+ @rhost = argvs['rhost']
72
+ @senderdomain = argvs['addresser'].host
73
+ @smtpagent = argvs['smtpagent']
74
+ @smtpcommand = argvs['smtpcommand']
75
+ @subject = argvs['subject']
76
+ @token = argvs['token']
77
+ @timestamp = argvs['timestamp']
78
+ @timezoneoffset = argvs['timezoneoffset']
79
+ end
80
+
81
+ # Constructor of Sisimai::Fact
82
+ # @param [Hash] argvs
83
+ # @options argvs [String] data Entire email message
84
+ # @options argvs [Boolean] delivered Include the result which has "delivered" reason
85
+ # @options argvs [Boolean] vacation Include the result which has "vacation" reason
86
+ # @options argvs [Proc] hook Proc object of callback method
87
+ # @options argvs [Array] load User defined MTA module list
88
+ # @options argvs [Array] order The order of MTA modules
89
+ # @options argvs [String] origin Path to the original email file
90
+ # @return [Array] Array of Sisimai::Fact objects
91
+ def self.rise(**argvs)
92
+ return nil unless argvs
93
+ return nil unless argvs.is_a? Hash
94
+
95
+ email = argvs[:data]; return nil unless email
96
+ loads = argvs[:load] || nil
97
+ order = argvs[:order] || nil
98
+ args1 = { data: email, hook: argvs[:hook], load: loads, order: order }
99
+ mesg1 = Sisimai::Message.rise(**args1)
100
+
101
+ return nil unless mesg1
102
+ return nil unless mesg1['ds']
103
+ return nil unless mesg1['rfc822']
104
+
105
+ deliveries = mesg1['ds'].dup
106
+ rfc822data = mesg1['rfc822']
107
+ listoffact = [];
108
+
109
+ while e = deliveries.shift do
110
+ # Create parameters for each Sisimai::Fact object
111
+ o = {} # To be passed to each accessor of Sisimai::Fact
112
+ p = {
113
+ 'action' => e['action'] || '',
114
+ 'alias' => e['alias'] || '',
115
+ 'catch' => mesg1['catch'] || nil,
116
+ 'deliverystatus' => e['status'] || '',
117
+ 'diagnosticcode' => e['diagnosis'] || '',
118
+ 'diagnostictype' => e['spec'] || '',
119
+ 'feedbacktype' => e['feedbacktype'] || '',
120
+ 'hardbounce' => false,
121
+ 'lhost' => e['lhost'] || '',
122
+ 'origin' => argvs[:origin],
123
+ 'reason' => e['reason'] || '',
124
+ 'recipient' => e['recipient'] || '',
125
+ 'replycode' => e['replycode'] || '',
126
+ 'rhost' => e['rhost'] || '',
127
+ 'smtpagent' => e['agent'] || '',
128
+ 'smtpcommand' => e['command'] || '',
129
+ }
130
+ unless argvs[:delivered]
131
+ # Skip if the value of "deliverystatus" begins with "2." such as 2.1.5
132
+ next if p['deliverystatus'].start_with?('2.')
133
+ end
134
+
135
+ unless argvs[:vacation]
136
+ # Skip if the value of "reason" is "vacation"
137
+ next if p['reason'] == 'vacation'
138
+ end
139
+
140
+ # EMAILADDRESS: Detect email address from message/rfc822 part
141
+ RFC822Head[:addresser].each do |f|
142
+ # Check each header in message/rfc822 part
143
+ g = f.downcase
144
+ next unless rfc822data[g]
145
+ next if rfc822data[g].empty?
146
+
147
+ j = Sisimai::Address.find(rfc822data[g]) || next
148
+ p['addresser'] = j.shift
149
+ break
150
+ end
151
+
152
+ unless p['addresser']
153
+ # Fallback: Get the sender address from the header of the bounced email if the address is
154
+ # not set at loop above.
155
+ j = Sisimai::Address.find(mesg1['header']['to']) || []
156
+ p['addresser'] = j.shift
157
+ end
158
+ next unless p['addresser']
159
+ next unless p['recipient']
160
+
161
+ # TIMESTAMP: Convert from a time stamp or a date string to a machine time.
162
+ datestring = nil
163
+ zoneoffset = 0
164
+ datevalues = []; datevalues << e['date'] unless e['date'].to_s.empty?
165
+
166
+ # Date information did not exist in message/delivery-status part,...
167
+ RFC822Head[:date].each do |f|
168
+ # Get the value of Date header or other date related header.
169
+ next unless rfc822data[f]
170
+ datevalues << rfc822data[f]
171
+ end
172
+
173
+ # Set "date" getting from the value of "Date" in the bounce message
174
+ datevalues << mesg1['header']['date'] if datevalues.size < 2
175
+
176
+ while v = datevalues.shift do
177
+ # Parse each date value in the array
178
+ datestring = Sisimai::DateTime.parse(v)
179
+ break if datestring
180
+ end
181
+
182
+ if datestring && cv = datestring.match(/\A(.+)[ ]+([-+]\d{4})\z/)
183
+ # Get the value of timezone offset from datestring: Wed, 26 Feb 2014 06:05:48 -0500
184
+ datestring = cv[1]
185
+ zoneoffset = Sisimai::DateTime.tz2second(cv[2])
186
+ p['timezoneoffset'] = cv[2]
187
+ end
188
+
189
+ begin
190
+ # Convert from the date string to an object then calculate time zone offset.
191
+ t = Sisimai::Time.strptime(datestring, '%a, %d %b %Y %T')
192
+ p['timestamp'] = (t.to_time.to_i - zoneoffset) || nil
193
+ rescue
194
+ warn ' ***warning: Failed to strptime ' << datestring.to_s
195
+ end
196
+ next unless p['timestamp']
197
+
198
+ # OTHER_TEXT_HEADERS:
199
+ recvheader = mesg1['header']['received'] || []
200
+ unless recvheader.empty?
201
+ # Get localhost and remote host name from Received header.
202
+ %w[lhost rhost].each { |a| e[a] ||= '' }
203
+ e['lhost'] = Sisimai::RFC5322.received(recvheader[0]).shift if e['lhost'].empty?
204
+ e['rhost'] = Sisimai::RFC5322.received(recvheader[-1]).pop if e['rhost'].empty?
205
+ end
206
+
207
+ # Remove square brackets and curly brackets from the host variable
208
+ %w[rhost lhost].each do |v|
209
+ p[v] = p[v].split('@')[-1] if p[v].include?('@')
210
+ p[v].delete!('[]()') # Remove square brackets and curly brackets from the host variable
211
+ p[v].sub!(/\A.+=/, '') # Remove string before "="
212
+ p[v].chomp!("\r") if p[v].end_with?("\r") # Remove CR at the end of the value
213
+
214
+ # Check space character in each value and get the first element
215
+ p[v] = p[v].split(' ', 2).shift if p[v].include?(' ')
216
+ p[v].chomp!('.') if p[v].end_with?('.') # Remove "." at the end of the value
217
+ end
218
+
219
+ # Subject: header of the original message
220
+ p['subject'] = rfc822data['subject'] || ''
221
+ p['subject'].scrub!('?')
222
+ p['subject'].chomp!("\r") if p['subject'].end_with?("\r")
223
+
224
+ # The value of "List-Id" header
225
+ if Sisimai::String.aligned(rfc822data['list-id'], ['<', '.', '>'])
226
+ # https://www.rfc-editor.org/rfc/rfc2919
227
+ # Get the value of List-Id header: "List name <list-id@example.org>"
228
+ p0 = rfc822data['list-id'].index('<') + 1
229
+ p1 = rfc822data['list-id'].index('>')
230
+ p['listid'] = rfc822data['list-id'][p0, p1 - p0]
231
+ else
232
+ # Invalid value of the List-Id: field
233
+ p['listid'] = ''
234
+ end
235
+
236
+ # The value of "Message-Id" header
237
+ if Sisimai::String.aligned(rfc822data['message-id'], ['<', '@', '>'])
238
+ # https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4
239
+ # Leave only string inside of angle brackets(<>)
240
+ p0 = rfc822data['message-id'].index('<') + 1
241
+ p1 = rfc822data['message-id'].index('>')
242
+ p['messageid'] = rfc822data['message-id'][p0, p1 - p0]
243
+ else
244
+ # Invalid value of the Message-Id: field
245
+ p['messageid'] = ''
246
+ end
247
+
248
+ # CHECK_DELIVERY_STATUS_VALUE: Cleanup the value of "Diagnostic-Code:" header
249
+ if p['diagnosticcode'].to_s.size > 0
250
+ # Get an SMTP Reply Code and an SMTP Enhanced Status Code
251
+ p['diagnosticcode'].chop if p['diagnosticcode'][-1, 1] == "\r"
252
+
253
+ cs = Sisimai::SMTP::Status.find(p['diagnosticcode']) || ''
254
+ cr = Sisimai::SMTP::Reply.find(p['diagnosticcode'], cs) || ''
255
+ p['deliverystatus'] = Sisimai::SMTP::Status.prefer(p['deliverystatus'], cs, cr)
256
+
257
+ if cr.size == 3
258
+ # There is an SMTP reply code in the error message
259
+ p['replycode'] = cr if p['replycode'].to_s.empty?
260
+
261
+ if p['diagnosticcode'].include?(cr + '-')
262
+ # 550-5.7.1 [192.0.2.222] Our system has detected that this message is
263
+ # 550-5.7.1 likely unsolicited mail. To reduce the amount of spam sent to Gmail,
264
+ # 550-5.7.1 this message has been blocked. Please visit
265
+ # 550 5.7.1 https://support.google.com/mail/answer/188131 for more information.
266
+ #
267
+ # kijitora@example.co.uk
268
+ # host c.eu.example.com [192.0.2.3]
269
+ # SMTP error from remote mail server after end of data:
270
+ # 553-SPF (Sender Policy Framework) domain authentication
271
+ # 553-fail. Refer to the Troubleshooting page at
272
+ # 553-http://www.symanteccloud.com/troubleshooting for more
273
+ # 553 information. (#5.7.1)
274
+ ['-', " "].each do |q|
275
+ # Remove strings: "550-5.7.1", and "550 5.7.1" from the error message
276
+ cx = sprintf("%s%s%s", cr, q, cs)
277
+ p0 = p['diagnosticcode'].index(cx)
278
+ while p0
279
+ # Remove strings like "550-5.7.1"
280
+ p['diagnosticcode'][p0, cx.size] = ''
281
+ p0 = p['diagnosticcode'].index(cx)
282
+ end
283
+
284
+ # Remove "553-" and "553 " (SMTP reply code only) from the error message
285
+ cx = sprintf("%s%s", cr, q)
286
+ p0 = p['diagnosticcode'].index(cx)
287
+ while p0
288
+ # Remove strings like "553-"
289
+ p['diagnosticcode'][p0, cx.size] = ''
290
+ p0 = p['diagnosticcode'].index(cx)
291
+ end
292
+ end
293
+
294
+ if p['diagnosticcode'].index(cr).to_i > 1
295
+ # Add "550 5.1.1" into the head of the error message when the error message does not
296
+ # begin with "550"
297
+ p['diagnosticcode'] = sprintf("%s %s %s", cr, cs, p['diagnosticcode'])
298
+ end
299
+ end
300
+ end
301
+
302
+ p1 = p['diagnosticcode'].downcase.index('<html>')
303
+ p2 = p['diagnosticcode'].downcase.index('</html>')
304
+ p['diagnosticcode'][p1, p2 + 7 - p1] = '' if p1 && p2
305
+ p['diagnosticcode'] = Sisimai::String.sweep(p['diagnosticcode'])
306
+ end
307
+
308
+ if Sisimai::String.is_8bit(p['diagnosticcode'])
309
+ # To avoid incompatible character encodings: ASCII-8BIT and UTF-8 (Encoding::CompatibilityError
310
+ p['diagnosticcode'] = p['diagnosticcode'].force_encoding('UTF-8').scrub('?')
311
+ end
312
+
313
+ p['diagnostictype'] = nil if p['diagnostictype'].empty?
314
+ p['diagnostictype'] ||= 'X-UNIX' if p['reason'] == 'mailererror'
315
+ p['diagnostictype'] ||= 'SMTP' unless %w[feedback vacation].include?(p['reason'])
316
+
317
+ # Check the value of SMTP command
318
+ p['smtpcommand'] = '' unless Sisimai::SMTP::Command.test(p['smtpcommand'])
319
+
320
+ # Create parameters for the constructor
321
+ as = Sisimai::Address.new(p['addresser']) || next; next if as.void
322
+ ar = Sisimai::Address.new(address: p['recipient']) || next; next if ar.void
323
+ ea = %w[
324
+ action deliverystatus diagnosticcode diagnostictype feedbacktype lhost listid messageid
325
+ origin reason replycode rhost smtpagent smtpcommand subject
326
+ ]
327
+
328
+ o = {
329
+ 'addresser' => as,
330
+ 'recipient' => ar,
331
+ 'senderdomain' => as.host,
332
+ 'destination' => ar.host,
333
+ 'alias' => p['alias'] || ar.alias,
334
+ 'token' => Sisimai::String.token(as.address, ar.address, p['timestamp']),
335
+ }
336
+
337
+ # Other accessors
338
+ ea.each { |q| o[q] ||= p[q] || '' }
339
+ o['catch'] = p['catch'] || nil
340
+ o['hardbounce'] = p['hardbounce']
341
+ o['replycode'] = Sisimai::SMTP::Reply.find(p['diagnosticcode']).to_s if o['replycode'].empty?
342
+ o['timestamp'] = Sisimai::Time.parse(::Time.at(p['timestamp']).to_s)
343
+ o['timezoneoffset'] = p['timezoneoffset'] || '+0000'
344
+
345
+ # REASON: Decide the reason of email bounce
346
+ if o['reason'].empty? || RetryIndex[o['reason']]
347
+ # The value of "reason" is empty or is needed to check with other values again
348
+ re = ''; de = o['destination']
349
+ re = Sisimai::Rhost.get(o) if Sisimai::Rhost.match(o['rhost'])
350
+ if re.empty?
351
+ # Failed to detect a bounce reason by the value of "rhost"
352
+ re = Sisimai::Rhost.get(o, de) if Sisimai::Rhost.match(de)
353
+ re = Sisimai::Reason.get(o) if re.empty?
354
+ re = 'undefined' if re.empty?
355
+ end
356
+ o['reason'] = re
357
+ end
358
+
359
+ # HARDBOUNCE: Set the value of "hardbounce", default value of "bouncebounce" is false
360
+ if o['reason'] == 'delivered' || o['reason'] == 'feedback' || o['reason'] == 'vacation'
361
+ # The value of "reason" is "delivered", "vacation" or "feedback".
362
+ o['replycode'] = '' unless o['reason'] == 'delivered'
363
+ else
364
+ smtperrors = p['deliverystatus'] + ' ' << p['diagnosticcode']
365
+ smtperrors = '' if smtperrors.size < 4
366
+ softorhard = Sisimai::SMTP::Error.soft_or_hard(o['reason'], smtperrors)
367
+ o['hardbounce'] = true if softorhard == 'hard'
368
+ end
369
+
370
+ # DELIVERYSTATUS: Set a pseudo status code if the value of "deliverystatus" is empty
371
+ if o['deliverystatus'].empty?
372
+ smtperrors = p['replycode'] + ' ' << p['diagnosticcode']
373
+ smtperrors = '' if smtperrors.size < 4
374
+ permanent1 = Sisimai::SMTP::Error.is_permanent(smtperrors)
375
+ permanent1 = true if permanent1 == nil
376
+ o['deliverystatus'] = Sisimai::SMTP::Status.code(o['reason'], permanent1 ? false : true) || ''
377
+ end
378
+
379
+ # REPLYCODE: Check both of the first digit of "deliverystatus" and "replycode"
380
+ cx = [o['deliverystatus'][0, 1], o['replycode'][0, 1]]
381
+ if cx[0] != cx[1]
382
+ # The class of the "Status:" is defer with the first digit of the reply code
383
+ cx[1] = Sisimai::SMTP::Reply.find(p['diagnosticcode'], cx[0]) || ''
384
+ o['replycode'] = cx[1].start_with?(cx[0]) ? cx[1] : ''
385
+ end
386
+
387
+ unless ActionList.has_key?(o['action'])
388
+ # There is an action value which is not described at RFC1894
389
+ if ox = Sisimai::RFC1894.field('Action: ' << o['action'])
390
+ # Rewrite the value of "Action:" field to the valid value
391
+ #
392
+ # The syntax for the action-field is:
393
+ # action-field = "Action" ":" action-value
394
+ # action-value = "failed" / "delayed" / "delivered" / "relayed" / "expanded"
395
+ o['action'] = ox[2]
396
+ end
397
+ end
398
+ o['action'] = 'delayed' if o['reason'] == 'expired'
399
+ if o['action'].empty?
400
+ o['action'] = 'failed' if cx[0] == '4' || cx[0] == '5'
401
+ end
402
+
403
+ listoffact << Sisimai::Fact.new(o)
404
+ end
405
+ return listoffact
406
+ end
407
+
408
+ # Emulate "softbounce" accessor for the backward compatible
409
+ # @return [Integer]
410
+ def softbounce
411
+ warn ' ***warning: Sisimai::Fact.softbounce will be removed at v5.1.0. Use Sisimai::Fact.hardbounce instead'
412
+ return 0 if self.hardbounce
413
+ return -1 if self.reason == 'delivered' || self.reason == 'feedback' || self.reason == 'vacation'
414
+ return 1
415
+ end
416
+
417
+ # Convert from Sisimai::Fact object to a Hash
418
+ # @return [Hash] Hashed data
419
+ def damn
420
+ data = {}
421
+ stringdata = %w[
422
+ action alias catch deliverystatus destination diagnosticcode diagnostictype feedbacktype
423
+ lhost listid messageid origin reason replycode rhost senderdomain smtpagent smtpcommand
424
+ subject timezoneoffset token
425
+ ]
426
+
427
+ begin
428
+ v = {}
429
+ stringdata.each { |e| v[e] = self.send(e.to_sym) || '' }
430
+ v['hardbounce'] = self.hardbounce
431
+ v['addresser'] = self.addresser.address
432
+ v['recipient'] = self.recipient.address
433
+ v['timestamp'] = self.timestamp.to_time.to_i
434
+ data = v
435
+ rescue
436
+ warn ' ***warning: Failed to execute Sisimai::Fact.damn'
437
+ end
438
+ return data
439
+ end
440
+ alias :to_hash :damn
441
+
442
+ # Data dumper
443
+ # @param [String] type Data format: json, yaml
444
+ # @return [String] data
445
+ # [Nil] The value of the first argument is neither "json" nor "yaml"
446
+ def dump(type = 'json')
447
+ return nil unless %w[json yaml].include?(type)
448
+ referclass = 'Sisimai::Fact::' << type.upcase
449
+
450
+ begin
451
+ require referclass.downcase.gsub('::', '/')
452
+ rescue
453
+ warn '***warning: Failed to load' << referclass
454
+ end
455
+
456
+ dumpeddata = Module.const_get(referclass).dump(self)
457
+ return dumpeddata
458
+ end
459
+
460
+ # JSON handler
461
+ # @return [String] JSON string converted from Sisimai::Fact
462
+ def to_json(*)
463
+ return self.dump('json')
464
+ end
465
+
466
+ end
467
+ end
468
+