sisimai 4.25.16-java → 5.0.2-java

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 (180) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/rake-test.yml +55 -0
  3. data/.travis.yml +3 -3
  4. data/ANALYTICAL-PRECISION +2 -2
  5. data/Benchmarks.mk +3 -3
  6. data/CONTRIBUTING +1 -1
  7. data/ChangeLog.md +451 -393
  8. data/Developers.mk +5 -6
  9. data/Gemfile +1 -1
  10. data/Makefile +15 -15
  11. data/README-JA.md +323 -149
  12. data/README.md +319 -149
  13. data/Rakefile +9 -3
  14. data/Repository.mk +2 -3
  15. data/lib/sisimai/address.rb +118 -74
  16. data/lib/sisimai/arf.rb +84 -82
  17. data/lib/sisimai/datetime.rb +5 -52
  18. data/lib/sisimai/{data → fact}/json.rb +7 -9
  19. data/lib/sisimai/fact/yaml.rb +31 -0
  20. data/lib/sisimai/fact.rb +506 -0
  21. data/lib/sisimai/lhost/activehunter.rb +12 -14
  22. data/lib/sisimai/lhost/amavis.rb +11 -14
  23. data/lib/sisimai/lhost/amazonses.rb +37 -42
  24. data/lib/sisimai/lhost/amazonworkmail.rb +15 -19
  25. data/lib/sisimai/lhost/aol.rb +12 -15
  26. data/lib/sisimai/lhost/apachejames.rb +19 -21
  27. data/lib/sisimai/lhost/barracuda.rb +10 -12
  28. data/lib/sisimai/lhost/bigfoot.rb +21 -22
  29. data/lib/sisimai/lhost/biglobe.rb +15 -16
  30. data/lib/sisimai/lhost/courier.rb +20 -20
  31. data/lib/sisimai/lhost/domino.rb +23 -20
  32. data/lib/sisimai/lhost/einsundeins.rb +23 -18
  33. data/lib/sisimai/lhost/exchange2003.rb +30 -29
  34. data/lib/sisimai/lhost/exchange2007.rb +70 -58
  35. data/lib/sisimai/lhost/exim.rb +179 -174
  36. data/lib/sisimai/lhost/ezweb.rb +31 -56
  37. data/lib/sisimai/lhost/facebook.rb +21 -34
  38. data/lib/sisimai/lhost/fml.rb +43 -48
  39. data/lib/sisimai/lhost/gmail.rb +29 -29
  40. data/lib/sisimai/lhost/gmx.rb +18 -17
  41. data/lib/sisimai/lhost/googlegroups.rb +11 -11
  42. data/lib/sisimai/lhost/gsuite.rb +21 -28
  43. data/lib/sisimai/lhost/imailserver.rb +25 -39
  44. data/lib/sisimai/lhost/interscanmss.rb +28 -31
  45. data/lib/sisimai/lhost/kddi.rb +22 -28
  46. data/lib/sisimai/lhost/mailfoundry.rb +11 -12
  47. data/lib/sisimai/lhost/mailmarshalsmtp.rb +25 -29
  48. data/lib/sisimai/lhost/mailru.rb +37 -40
  49. data/lib/sisimai/lhost/mcafee.rb +21 -31
  50. data/lib/sisimai/lhost/messagelabs.rb +17 -21
  51. data/lib/sisimai/lhost/messagingserver.rb +40 -37
  52. data/lib/sisimai/lhost/mfilter.rb +16 -17
  53. data/lib/sisimai/lhost/mxlogic.rb +24 -33
  54. data/lib/sisimai/lhost/notes.rb +17 -17
  55. data/lib/sisimai/lhost/office365.rb +64 -28
  56. data/lib/sisimai/lhost/opensmtpd.rb +12 -13
  57. data/lib/sisimai/lhost/outlook.rb +12 -16
  58. data/lib/sisimai/lhost/postfix.rb +179 -130
  59. data/lib/sisimai/lhost/powermta.rb +12 -14
  60. data/lib/sisimai/lhost/qmail.rb +44 -47
  61. data/lib/sisimai/lhost/receivingses.rb +15 -21
  62. data/lib/sisimai/lhost/sendgrid.rb +34 -34
  63. data/lib/sisimai/lhost/sendmail.rb +65 -53
  64. data/lib/sisimai/lhost/surfcontrol.rb +19 -19
  65. data/lib/sisimai/lhost/v5sendmail.rb +45 -39
  66. data/lib/sisimai/lhost/verizon.rb +35 -39
  67. data/lib/sisimai/lhost/x1.rb +18 -17
  68. data/lib/sisimai/lhost/x2.rb +17 -14
  69. data/lib/sisimai/lhost/x3.rb +19 -19
  70. data/lib/sisimai/lhost/x4.rb +72 -57
  71. data/lib/sisimai/lhost/x5.rb +17 -19
  72. data/lib/sisimai/lhost/x6.rb +41 -17
  73. data/lib/sisimai/lhost/yahoo.rb +17 -16
  74. data/lib/sisimai/lhost/yandex.rb +16 -21
  75. data/lib/sisimai/lhost/zoho.rb +16 -15
  76. data/lib/sisimai/lhost.rb +8 -10
  77. data/lib/sisimai/mail/maildir.rb +1 -3
  78. data/lib/sisimai/mail/mbox.rb +3 -4
  79. data/lib/sisimai/mail/memory.rb +0 -1
  80. data/lib/sisimai/mail/stdin.rb +1 -3
  81. data/lib/sisimai/mail.rb +3 -7
  82. data/lib/sisimai/mda.rb +28 -42
  83. data/lib/sisimai/message.rb +444 -326
  84. data/lib/sisimai/order.rb +5 -5
  85. data/lib/sisimai/reason/authfailure.rb +65 -0
  86. data/lib/sisimai/reason/badreputation.rb +53 -0
  87. data/lib/sisimai/reason/blocked.rb +96 -160
  88. data/lib/sisimai/reason/contenterror.rb +8 -9
  89. data/lib/sisimai/reason/delivered.rb +4 -6
  90. data/lib/sisimai/reason/exceedlimit.rb +10 -12
  91. data/lib/sisimai/reason/expired.rb +7 -8
  92. data/lib/sisimai/reason/feedback.rb +2 -3
  93. data/lib/sisimai/reason/filtered.rb +17 -19
  94. data/lib/sisimai/reason/hasmoved.rb +9 -10
  95. data/lib/sisimai/reason/hostunknown.rb +15 -15
  96. data/lib/sisimai/reason/mailboxfull.rb +11 -12
  97. data/lib/sisimai/reason/mailererror.rb +18 -20
  98. data/lib/sisimai/reason/mesgtoobig.rb +9 -11
  99. data/lib/sisimai/reason/networkerror.rb +5 -8
  100. data/lib/sisimai/reason/norelaying.rb +8 -11
  101. data/lib/sisimai/reason/notaccept.rb +13 -14
  102. data/lib/sisimai/reason/notcompliantrfc.rb +43 -0
  103. data/lib/sisimai/reason/onhold.rb +6 -9
  104. data/lib/sisimai/reason/policyviolation.rb +14 -12
  105. data/lib/sisimai/reason/rejected.rb +26 -24
  106. data/lib/sisimai/reason/requireptr.rb +69 -0
  107. data/lib/sisimai/reason/securityerror.rb +34 -36
  108. data/lib/sisimai/reason/spamdetected.rb +115 -147
  109. data/lib/sisimai/reason/speeding.rb +49 -0
  110. data/lib/sisimai/reason/suspend.rb +12 -11
  111. data/lib/sisimai/reason/syntaxerror.rb +11 -10
  112. data/lib/sisimai/reason/systemerror.rb +7 -9
  113. data/lib/sisimai/reason/systemfull.rb +7 -8
  114. data/lib/sisimai/reason/toomanyconn.rb +9 -11
  115. data/lib/sisimai/reason/undefined.rb +2 -3
  116. data/lib/sisimai/reason/userunknown.rb +129 -146
  117. data/lib/sisimai/reason/vacation.rb +3 -4
  118. data/lib/sisimai/reason/virusdetected.rb +10 -11
  119. data/lib/sisimai/reason.rb +59 -64
  120. data/lib/sisimai/rfc1894.rb +55 -28
  121. data/lib/sisimai/rfc2045.rb +373 -0
  122. data/lib/sisimai/rfc3464.rb +250 -308
  123. data/lib/sisimai/rfc3834.rb +42 -45
  124. data/lib/sisimai/rfc5322.rb +177 -146
  125. data/lib/sisimai/rfc5965.rb +31 -0
  126. data/lib/sisimai/rhost/cox.rb +5 -6
  127. data/lib/sisimai/rhost/franceptt.rb +6 -8
  128. data/lib/sisimai/rhost/godaddy.rb +12 -12
  129. data/lib/sisimai/rhost/google.rb +530 -0
  130. data/lib/sisimai/rhost/iua.rb +9 -10
  131. data/lib/sisimai/rhost/kddi.rb +6 -8
  132. data/lib/sisimai/rhost/{exchangeonline.rb → microsoft.rb} +115 -114
  133. data/lib/sisimai/rhost/mimecast.rb +51 -42
  134. data/lib/sisimai/rhost/nttdocomo.rb +12 -12
  135. data/lib/sisimai/rhost/spectrum.rb +10 -12
  136. data/lib/sisimai/rhost/{tencentqq.rb → tencent.rb} +7 -8
  137. data/lib/sisimai/rhost.rb +23 -31
  138. data/lib/sisimai/smtp/command.rb +59 -0
  139. data/lib/sisimai/smtp/error.rb +4 -7
  140. data/lib/sisimai/smtp/reply.rb +161 -74
  141. data/lib/sisimai/smtp/status.rb +507 -393
  142. data/lib/sisimai/smtp/transcript.rb +124 -0
  143. data/lib/sisimai/smtp.rb +0 -1
  144. data/lib/sisimai/string.rb +74 -5
  145. data/lib/sisimai/time.rb +1 -2
  146. data/lib/sisimai/version.rb +1 -1
  147. data/lib/sisimai.rb +46 -31
  148. data/set-of-emails/maildir/bsd/lhost-domino-02.eml +6 -3
  149. data/set-of-emails/maildir/bsd/lhost-googlegroups-15.eml +174 -0
  150. data/set-of-emails/maildir/bsd/lhost-gsuite-15.eml +229 -0
  151. data/set-of-emails/maildir/bsd/lhost-postfix-75.eml +51 -0
  152. data/set-of-emails/maildir/bsd/lhost-postfix-76.eml +101 -0
  153. data/set-of-emails/maildir/bsd/lhost-postfix-77.eml +74 -0
  154. data/set-of-emails/maildir/bsd/lhost-postfix-78.eml +91 -0
  155. data/set-of-emails/maildir/bsd/lhost-receivingses-08.eml +88 -0
  156. data/set-of-emails/maildir/bsd/lhost-sendmail-60.eml +85 -0
  157. data/set-of-emails/maildir/bsd/rfc3464-43.eml +88 -0
  158. data/set-of-emails/maildir/bsd/rhost-google-03.eml +101 -0
  159. data/set-of-emails/maildir/bsd/rhost-google-04.eml +102 -0
  160. data/set-of-emails/maildir/bsd/rhost-google-05.eml +82 -0
  161. data/set-of-emails/maildir/bsd/rhost-google-06.eml +102 -0
  162. data/set-of-emails/maildir/bsd/rhost-google-07.eml +69 -0
  163. data/set-of-emails/maildir/bsd/rhost-google-08.eml +99 -0
  164. data/sisimai-java.gemspec +1 -1
  165. data/sisimai.gemspec +1 -1
  166. metadata +48 -26
  167. data/.rspec +0 -2
  168. data/lib/sisimai/data/yaml.rb +0 -33
  169. data/lib/sisimai/data.rb +0 -411
  170. data/lib/sisimai/mime.rb +0 -456
  171. data/lib/sisimai/rhost/googleapps.rb +0 -261
  172. /data/set-of-emails/maildir/bsd/{rfc3464-41.eml → rfc3834-05.eml} +0 -0
  173. /data/set-of-emails/maildir/bsd/{rhost-googleapps-01.eml → rhost-google-01.eml} +0 -0
  174. /data/set-of-emails/maildir/bsd/{rhost-googleapps-02.eml → rhost-google-02.eml} +0 -0
  175. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-01.eml → rhost-microsoft-01.eml} +0 -0
  176. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-02.eml → rhost-microsoft-02.eml} +0 -0
  177. /data/set-of-emails/maildir/bsd/{rhost-exchangeonline-03.eml → rhost-microsoft-03.eml} +0 -0
  178. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-01.eml → rhost-tencent-01.eml} +0 -0
  179. /data/set-of-emails/maildir/bsd/{rhost-tencentqq-02.eml → rhost-tencent-02.eml} +0 -0
  180. /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,506 @@
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
+ if RUBY_PLATFORM.start_with?('java')
50
+ # [WORKAROUND] #159 #267 JRuby seems to fail and throws exception at strptime(), but this
51
+ # issue might be fixed in a future version of JRuby.
52
+ # https://gist.github.com/hiroyuki-sato/6ef40245874d4c847a95ef99886e4fa7
53
+ # https://github.com/sisimai/rb-sisimai/issues/267#issuecomment-1976642884
54
+ # https://github.com/jruby/jruby/issues/8139
55
+ # https://github.com/sisimai/rb-sisimai/issues/267
56
+ TimeModule = ::DateTime
57
+ else
58
+ TimeModule = Sisimai::Time
59
+ end
60
+
61
+ # Constructor of Sisimai::Fact
62
+ # @param [Hash] argvs Including each parameter
63
+ # @return [Sisimai::Fact] Structured email data
64
+ def initialize(argvs)
65
+ # Create email address object
66
+ @alias = argvs['alias'] || ''
67
+ @addresser = argvs['addresser']
68
+ @action = argvs['action']
69
+ @catch = argvs['catch']
70
+ @diagnosticcode = argvs['diagnosticcode']
71
+ @diagnostictype = argvs['diagnostictype']
72
+ @deliverystatus = argvs['deliverystatus']
73
+ @destination = argvs['recipient'].host
74
+ @feedbacktype = argvs['feedbacktype']
75
+ @hardbounce = argvs['hardbounce']
76
+ @lhost = argvs['lhost']
77
+ @listid = argvs['listid']
78
+ @messageid = argvs['messageid']
79
+ @origin = argvs['origin']
80
+ @reason = argvs['reason']
81
+ @recipient = argvs['recipient']
82
+ @replycode = argvs['replycode']
83
+ @rhost = argvs['rhost']
84
+ @senderdomain = argvs['addresser'].host
85
+ @smtpagent = argvs['smtpagent']
86
+ @smtpcommand = argvs['smtpcommand']
87
+ @subject = argvs['subject']
88
+ @token = argvs['token']
89
+ @timestamp = argvs['timestamp']
90
+ @timezoneoffset = argvs['timezoneoffset']
91
+ end
92
+
93
+ # Constructor of Sisimai::Fact
94
+ # @param [Hash] argvs
95
+ # @options argvs [String] data Entire email message
96
+ # @options argvs [Boolean] delivered Include the result which has "delivered" reason
97
+ # @options argvs [Boolean] vacation Include the result which has "vacation" reason
98
+ # @options argvs [Proc] hook Proc object of callback method
99
+ # @options argvs [Array] load User defined MTA module list
100
+ # @options argvs [Array] order The order of MTA modules
101
+ # @options argvs [String] origin Path to the original email file
102
+ # @return [Array] Array of Sisimai::Fact objects
103
+ def self.rise(**argvs)
104
+ return nil unless argvs
105
+ return nil unless argvs.is_a? Hash
106
+
107
+ email = argvs[:data]; return nil unless email
108
+ loads = argvs[:load] || nil
109
+ order = argvs[:order] || nil
110
+ args1 = { data: email, hook: argvs[:hook], load: loads, order: order }
111
+ mesg1 = Sisimai::Message.rise(**args1)
112
+
113
+ return nil unless mesg1
114
+ return nil unless mesg1['ds']
115
+ return nil unless mesg1['rfc822']
116
+
117
+ deliveries = mesg1['ds'].dup
118
+ rfc822data = mesg1['rfc822']
119
+ listoffact = [];
120
+
121
+ while e = deliveries.shift do
122
+ # Create parameters for each Sisimai::Fact object
123
+ o = {} # To be passed to each accessor of Sisimai::Fact
124
+ p = {
125
+ 'action' => e['action'] || '',
126
+ 'alias' => e['alias'] || '',
127
+ 'catch' => mesg1['catch'] || nil,
128
+ 'deliverystatus' => e['status'] || '',
129
+ 'diagnosticcode' => e['diagnosis'] || '',
130
+ 'diagnostictype' => e['spec'] || '',
131
+ 'feedbacktype' => e['feedbacktype'] || '',
132
+ 'hardbounce' => false,
133
+ 'lhost' => e['lhost'] || '',
134
+ 'origin' => argvs[:origin],
135
+ 'reason' => e['reason'] || '',
136
+ 'recipient' => e['recipient'] || '',
137
+ 'replycode' => e['replycode'] || '',
138
+ 'rhost' => e['rhost'] || '',
139
+ 'smtpagent' => e['agent'] || '',
140
+ 'smtpcommand' => e['command'] || '',
141
+ }
142
+ unless argvs[:delivered]
143
+ # Skip if the value of "deliverystatus" begins with "2." such as 2.1.5
144
+ next if p['deliverystatus'].start_with?('2.')
145
+ end
146
+
147
+ unless argvs[:vacation]
148
+ # Skip if the value of "reason" is "vacation"
149
+ next if p['reason'] == 'vacation'
150
+ end
151
+
152
+ # EMAILADDRESS: Detect email address from message/rfc822 part
153
+ RFC822Head[:addresser].each do |f|
154
+ # Check each header in message/rfc822 part
155
+ g = f.downcase
156
+ next unless rfc822data[g]
157
+ next if rfc822data[g].empty?
158
+
159
+ j = Sisimai::Address.find(rfc822data[g]) || next
160
+ p['addresser'] = j.shift
161
+ break
162
+ end
163
+
164
+ unless p['addresser']
165
+ # Fallback: Get the sender address from the header of the bounced email if the address is
166
+ # not set at loop above.
167
+ j = Sisimai::Address.find(mesg1['header']['to']) || []
168
+ p['addresser'] = j.shift
169
+ end
170
+ next unless p['addresser']
171
+ next unless p['recipient']
172
+
173
+ # TIMESTAMP: Convert from a time stamp or a date string to a machine time.
174
+ datestring = nil
175
+ zoneoffset = 0
176
+ datevalues = []; datevalues << e['date'] unless e['date'].to_s.empty?
177
+
178
+ # Date information did not exist in message/delivery-status part,...
179
+ RFC822Head[:date].each do |f|
180
+ # Get the value of Date header or other date related header.
181
+ next unless rfc822data[f]
182
+ datevalues << rfc822data[f]
183
+ end
184
+
185
+ # Set "date" getting from the value of "Date" in the bounce message
186
+ datevalues << mesg1['header']['date'] if datevalues.size < 2
187
+
188
+ while v = datevalues.shift do
189
+ # Parse each date value in the array
190
+ datestring = Sisimai::DateTime.parse(v)
191
+ break if datestring
192
+ end
193
+
194
+ if datestring && cv = datestring.match(/\A(.+)[ ]+([-+]\d{4})\z/)
195
+ # Get the value of timezone offset from datestring: Wed, 26 Feb 2014 06:05:48 -0500
196
+ datestring = cv[1]
197
+ zoneoffset = Sisimai::DateTime.tz2second(cv[2])
198
+ p['timezoneoffset'] = cv[2]
199
+ end
200
+
201
+ begin
202
+ # Convert from the date string to an object then calculate time zone offset.
203
+ t = TimeModule.strptime(datestring, '%a, %d %b %Y %T')
204
+ p['timestamp'] = (t.to_time.to_i - zoneoffset) || nil
205
+ rescue
206
+ warn ' ***warning: Failed to strptime ' << datestring.to_s
207
+ end
208
+ next unless p['timestamp']
209
+
210
+ # OTHER_TEXT_HEADERS:
211
+ rr = mesg1['header']['received'] || []
212
+ unless rr.empty?
213
+ # Get a localhost and a remote host name from Received header.
214
+ p['rhost'] = Sisimai::RFC5322.received(rr[-1])[1] if p['rhost'].empty?
215
+ p['lhost'] = '' if p['rhost'] == p['lhost']
216
+ p['lhost'] = Sisimai::RFC5322.received(rr[ 0])[0] if p['lhost'].empty?
217
+ end
218
+
219
+ # Remove square brackets and curly brackets from the host variable
220
+ %w[rhost lhost].each do |v|
221
+ p[v] = p[v].split('@')[-1] if p[v].include?('@')
222
+ p[v].delete!('[]()') # Remove square brackets and curly brackets from the host variable
223
+ p[v].sub!(/\A.+=/, '') # Remove string before "="
224
+ p[v].chomp!("\r") if p[v].end_with?("\r") # Remove CR at the end of the value
225
+
226
+ # Check space character in each value and get the first element
227
+ p[v] = p[v].split(' ', 2).shift if p[v].include?(' ')
228
+ p[v].chomp!('.') if p[v].end_with?('.') # Remove "." at the end of the value
229
+ end
230
+
231
+ # Subject: header of the original message
232
+ p['subject'] = rfc822data['subject'] || ''
233
+ p['subject'].scrub!('?')
234
+ p['subject'].chomp!("\r") if p['subject'].end_with?("\r")
235
+
236
+ # The value of "List-Id" header
237
+ if Sisimai::String.aligned(rfc822data['list-id'], ['<', '.', '>'])
238
+ # https://www.rfc-editor.org/rfc/rfc2919
239
+ # Get the value of List-Id header: "List name <list-id@example.org>"
240
+ p0 = rfc822data['list-id'].index('<') + 1
241
+ p1 = rfc822data['list-id'].index('>')
242
+ p['listid'] = rfc822data['list-id'][p0, p1 - p0]
243
+ else
244
+ # Invalid value of the List-Id: field
245
+ p['listid'] = ''
246
+ end
247
+
248
+ # The value of "Message-Id" header
249
+ if Sisimai::String.aligned(rfc822data['message-id'], ['<', '@', '>'])
250
+ # https://www.rfc-editor.org/rfc/rfc5322#section-3.6.4
251
+ # Leave only string inside of angle brackets(<>)
252
+ p0 = rfc822data['message-id'].index('<') + 1
253
+ p1 = rfc822data['message-id'].index('>')
254
+ p['messageid'] = rfc822data['message-id'][p0, p1 - p0]
255
+ else
256
+ # Invalid value of the Message-Id: field
257
+ p['messageid'] = ''
258
+ end
259
+
260
+ # CHECK_DELIVERY_STATUS_VALUE: Cleanup the value of "Diagnostic-Code:" header
261
+ if p['diagnosticcode'].to_s.size > 0
262
+ # Get an SMTP Reply Code and an SMTP Enhanced Status Code
263
+ p['diagnosticcode'].chop if p['diagnosticcode'][-1, 1] == "\r"
264
+
265
+ cs = Sisimai::SMTP::Status.find(p['diagnosticcode']) || ''
266
+ cr = Sisimai::SMTP::Reply.find(p['diagnosticcode'], cs) || ''
267
+ p['deliverystatus'] = Sisimai::SMTP::Status.prefer(p['deliverystatus'], cs, cr)
268
+
269
+ if cr.size == 3
270
+ # There is an SMTP reply code in the error message
271
+ p['replycode'] = cr if p['replycode'].to_s.empty?
272
+
273
+ if p['diagnosticcode'].include?(cr + '-')
274
+ # 550-5.7.1 [192.0.2.222] Our system has detected that this message is
275
+ # 550-5.7.1 likely unsolicited mail. To reduce the amount of spam sent to Gmail,
276
+ # 550-5.7.1 this message has been blocked. Please visit
277
+ # 550 5.7.1 https://support.google.com/mail/answer/188131 for more information.
278
+ #
279
+ # kijitora@example.co.uk
280
+ # host c.eu.example.com [192.0.2.3]
281
+ # SMTP error from remote mail server after end of data:
282
+ # 553-SPF (Sender Policy Framework) domain authentication
283
+ # 553-fail. Refer to the Troubleshooting page at
284
+ # 553-http://www.symanteccloud.com/troubleshooting for more
285
+ # 553 information. (#5.7.1)
286
+ ['-', " "].each do |q|
287
+ # Remove strings: "550-5.7.1", and "550 5.7.1" from the error message
288
+ cx = sprintf("%s%s%s", cr, q, cs)
289
+ p0 = p['diagnosticcode'].index(cx)
290
+ while p0
291
+ # Remove strings like "550-5.7.1"
292
+ p['diagnosticcode'][p0, cx.size] = ''
293
+ p0 = p['diagnosticcode'].index(cx)
294
+ end
295
+
296
+ # Remove "553-" and "553 " (SMTP reply code only) from the error message
297
+ cx = sprintf("%s%s", cr, q)
298
+ p0 = p['diagnosticcode'].index(cx)
299
+ while p0
300
+ # Remove strings like "553-"
301
+ p['diagnosticcode'][p0, cx.size] = ''
302
+ p0 = p['diagnosticcode'].index(cx)
303
+ end
304
+ end
305
+
306
+ if p['diagnosticcode'].index(cr).to_i > 1
307
+ # Add "550 5.1.1" into the head of the error message when the error message does not
308
+ # begin with "550"
309
+ p['diagnosticcode'] = sprintf("%s %s %s", cr, cs, p['diagnosticcode'])
310
+ end
311
+ end
312
+ end
313
+
314
+ p1 = p['diagnosticcode'].downcase.index('<html>')
315
+ p2 = p['diagnosticcode'].downcase.index('</html>')
316
+ p['diagnosticcode'][p1, p2 + 7 - p1] = '' if p1 && p2
317
+ p['diagnosticcode'] = Sisimai::String.sweep(p['diagnosticcode'])
318
+ end
319
+
320
+ if Sisimai::String.is_8bit(p['diagnosticcode'])
321
+ # To avoid incompatible character encodings: ASCII-8BIT and UTF-8 (Encoding::CompatibilityError
322
+ p['diagnosticcode'] = p['diagnosticcode'].force_encoding('UTF-8').scrub('?')
323
+ end
324
+
325
+ p['diagnostictype'] = nil if p['diagnostictype'].empty?
326
+ p['diagnostictype'] ||= 'X-UNIX' if p['reason'] == 'mailererror'
327
+ p['diagnostictype'] ||= 'SMTP' unless %w[feedback vacation].include?(p['reason'])
328
+
329
+ # Check the value of SMTP command
330
+ p['smtpcommand'] = '' unless Sisimai::SMTP::Command.test(p['smtpcommand'])
331
+
332
+ # Create parameters for the constructor
333
+ as = Sisimai::Address.new(p['addresser']) || next; next if as.void
334
+ ar = Sisimai::Address.new(address: p['recipient']) || next; next if ar.void
335
+ ea = %w[
336
+ action deliverystatus diagnosticcode diagnostictype feedbacktype lhost listid messageid
337
+ origin reason replycode rhost smtpagent smtpcommand subject
338
+ ]
339
+
340
+ o = {
341
+ 'addresser' => as,
342
+ 'recipient' => ar,
343
+ 'senderdomain' => as.host,
344
+ 'destination' => ar.host,
345
+ 'alias' => p['alias'] || ar.alias,
346
+ 'token' => Sisimai::String.token(as.address, ar.address, p['timestamp']),
347
+ }
348
+
349
+ # Other accessors
350
+ ea.each { |q| o[q] ||= p[q] || '' }
351
+ o['catch'] = p['catch'] || nil
352
+ o['hardbounce'] = p['hardbounce']
353
+ o['replycode'] = Sisimai::SMTP::Reply.find(p['diagnosticcode']).to_s if o['replycode'].empty?
354
+ o['timestamp'] = TimeModule.parse(::Time.at(p['timestamp']).to_s)
355
+ o['timezoneoffset'] = p['timezoneoffset'] || '+0000'
356
+
357
+ # ALIAS
358
+ while true do
359
+ # Look up the Envelope-To address from the Received: header in the original message
360
+ # when the recipient address is same with the value of o['alias'].
361
+ break if o['alias'].empty?
362
+ break if o['recipient'].address != o['alias']
363
+ break unless rfc822data.has_key?('received')
364
+ break if rfc822data['received'].empty?
365
+
366
+ rfc822data['received'].reverse.each do |er|
367
+ # Search for the string " for " from the Received: header
368
+ next unless er.include?(' for ')
369
+
370
+ af = Sisimai::RFC5322.received(er)
371
+ next if af.empty?
372
+ next if af[5].empty?
373
+ next unless Sisimai::Address.is_emailaddress(af[5])
374
+ next if o['recipient'].address == af[5]
375
+
376
+ o['alias'] = af[5]
377
+ break
378
+ end
379
+ break
380
+ end
381
+ o['alias'] = '' if o['alias'] == o['recipient'].address
382
+
383
+ # REASON: Decide the reason of email bounce
384
+ if o['reason'].empty? || RetryIndex[o['reason']]
385
+ # The value of "reason" is empty or is needed to check with other values again
386
+ re = ''; de = o['destination']
387
+ re = Sisimai::Rhost.get(o) if Sisimai::Rhost.match(o['rhost'])
388
+ if re.empty?
389
+ # Failed to detect a bounce reason by the value of "rhost"
390
+ re = Sisimai::Rhost.get(o, de) if Sisimai::Rhost.match(de)
391
+ re = Sisimai::Reason.get(o) if re.empty?
392
+ re = 'undefined' if re.empty?
393
+ end
394
+ o['reason'] = re
395
+ end
396
+
397
+ # HARDBOUNCE: Set the value of "hardbounce", default value of "bouncebounce" is false
398
+ if o['reason'] == 'delivered' || o['reason'] == 'feedback' || o['reason'] == 'vacation'
399
+ # The value of "reason" is "delivered", "vacation" or "feedback".
400
+ o['replycode'] = '' unless o['reason'] == 'delivered'
401
+ else
402
+ smtperrors = p['deliverystatus'] + ' ' << p['diagnosticcode']
403
+ smtperrors = '' if smtperrors.size < 4
404
+ softorhard = Sisimai::SMTP::Error.soft_or_hard(o['reason'], smtperrors)
405
+ o['hardbounce'] = true if softorhard == 'hard'
406
+ end
407
+
408
+ # DELIVERYSTATUS: Set a pseudo status code if the value of "deliverystatus" is empty
409
+ if o['deliverystatus'].empty?
410
+ smtperrors = p['replycode'] + ' ' << p['diagnosticcode']
411
+ smtperrors = '' if smtperrors.size < 4
412
+ permanent1 = Sisimai::SMTP::Error.is_permanent(smtperrors)
413
+ permanent1 = true if permanent1 == nil
414
+ o['deliverystatus'] = Sisimai::SMTP::Status.code(o['reason'], permanent1 ? false : true) || ''
415
+ end
416
+
417
+ # REPLYCODE: Check both of the first digit of "deliverystatus" and "replycode"
418
+ cx = [o['deliverystatus'][0, 1], o['replycode'][0, 1]]
419
+ if cx[0] != cx[1]
420
+ # The class of the "Status:" is defer with the first digit of the reply code
421
+ cx[1] = Sisimai::SMTP::Reply.find(p['diagnosticcode'], cx[0]) || ''
422
+ o['replycode'] = cx[1].start_with?(cx[0]) ? cx[1] : ''
423
+ end
424
+
425
+ unless ActionList.has_key?(o['action'])
426
+ # There is an action value which is not described at RFC1894
427
+ if ox = Sisimai::RFC1894.field('Action: ' << o['action'])
428
+ # Rewrite the value of "Action:" field to the valid value
429
+ #
430
+ # The syntax for the action-field is:
431
+ # action-field = "Action" ":" action-value
432
+ # action-value = "failed" / "delayed" / "delivered" / "relayed" / "expanded"
433
+ o['action'] = ox[2]
434
+ end
435
+ end
436
+ o['action'] = 'delayed' if o['reason'] == 'expired'
437
+ if o['action'].empty?
438
+ o['action'] = 'failed' if cx[0] == '4' || cx[0] == '5'
439
+ end
440
+
441
+ listoffact << Sisimai::Fact.new(o)
442
+ end
443
+ return listoffact
444
+ end
445
+
446
+ # Emulate "softbounce" accessor for the backward compatible
447
+ # @return [Integer]
448
+ def softbounce
449
+ warn ' ***warning: Sisimai::Fact.softbounce will be removed at v5.1.0. Use Sisimai::Fact.hardbounce instead'
450
+ return 0 if self.hardbounce
451
+ return -1 if self.reason == 'delivered' || self.reason == 'feedback' || self.reason == 'vacation'
452
+ return 1
453
+ end
454
+
455
+ # Convert from Sisimai::Fact object to a Hash
456
+ # @return [Hash] Hashed data
457
+ def damn
458
+ data = {}
459
+ stringdata = %w[
460
+ action alias catch deliverystatus destination diagnosticcode diagnostictype feedbacktype
461
+ lhost listid messageid origin reason replycode rhost senderdomain smtpagent smtpcommand
462
+ subject timezoneoffset token
463
+ ]
464
+
465
+ begin
466
+ v = {}
467
+ stringdata.each { |e| v[e] = self.send(e.to_sym) || '' }
468
+ v['hardbounce'] = self.hardbounce
469
+ v['addresser'] = self.addresser.address
470
+ v['recipient'] = self.recipient.address
471
+ v['timestamp'] = self.timestamp.to_time.to_i
472
+ data = v
473
+ rescue
474
+ warn ' ***warning: Failed to execute Sisimai::Fact.damn'
475
+ end
476
+ return data
477
+ end
478
+ alias :to_hash :damn
479
+
480
+ # Data dumper
481
+ # @param [String] type Data format: json, yaml
482
+ # @return [String] data
483
+ # [Nil] The value of the first argument is neither "json" nor "yaml"
484
+ def dump(type = 'json')
485
+ return nil unless %w[json yaml].include?(type)
486
+ referclass = 'Sisimai::Fact::' << type.upcase
487
+
488
+ begin
489
+ require referclass.downcase.gsub('::', '/')
490
+ rescue
491
+ warn '***warning: Failed to load' << referclass
492
+ end
493
+
494
+ dumpeddata = Module.const_get(referclass).dump(self)
495
+ return dumpeddata
496
+ end
497
+
498
+ # JSON handler
499
+ # @return [String] JSON string converted from Sisimai::Fact
500
+ def to_json(*)
501
+ return self.dump('json')
502
+ end
503
+
504
+ end
505
+ end
506
+