sisimai 5.2.1-java → 5.4.0-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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake-test.yml +1 -1
  3. data/ChangeLog.md +24 -0
  4. data/Makefile +3 -2
  5. data/README-JA.md +4 -4
  6. data/README.md +8 -8
  7. data/lib/sisimai/address.rb +45 -56
  8. data/lib/sisimai/arf.rb +11 -16
  9. data/lib/sisimai/datetime.rb +16 -50
  10. data/lib/sisimai/fact/json.rb +5 -5
  11. data/lib/sisimai/fact/yaml.rb +3 -3
  12. data/lib/sisimai/fact.rb +21 -12
  13. data/lib/sisimai/lda.rb +3 -3
  14. data/lib/sisimai/lhost/activehunter.rb +4 -6
  15. data/lib/sisimai/lhost/amazonses.rb +5 -6
  16. data/lib/sisimai/lhost/apachejames.rb +7 -9
  17. data/lib/sisimai/lhost/biglobe.rb +3 -5
  18. data/lib/sisimai/lhost/courier.rb +4 -6
  19. data/lib/sisimai/lhost/domino.rb +4 -5
  20. data/lib/sisimai/lhost/dragonfly.rb +3 -5
  21. data/lib/sisimai/lhost/einsundeins.rb +6 -8
  22. data/lib/sisimai/lhost/exchange2003.rb +10 -12
  23. data/lib/sisimai/lhost/exchange2007.rb +4 -5
  24. data/lib/sisimai/lhost/exim.rb +6 -8
  25. data/lib/sisimai/lhost/ezweb.rb +10 -12
  26. data/lib/sisimai/lhost/fml.rb +2 -3
  27. data/lib/sisimai/lhost/gmail.rb +4 -6
  28. data/lib/sisimai/lhost/gmx.rb +6 -8
  29. data/lib/sisimai/lhost/googlegroups.rb +1 -2
  30. data/lib/sisimai/lhost/googleworkspace.rb +3 -4
  31. data/lib/sisimai/lhost/imailserver.rb +6 -7
  32. data/lib/sisimai/lhost/interscanmss.rb +1 -2
  33. data/lib/sisimai/lhost/kddi.rb +5 -8
  34. data/lib/sisimai/lhost/mailfoundry.rb +4 -7
  35. data/lib/sisimai/lhost/mailmarshalsmtp.rb +4 -6
  36. data/lib/sisimai/lhost/messagingserver.rb +5 -7
  37. data/lib/sisimai/lhost/mfilter.rb +4 -6
  38. data/lib/sisimai/lhost/notes.rb +7 -9
  39. data/lib/sisimai/lhost/opensmtpd.rb +2 -4
  40. data/lib/sisimai/lhost/postfix.rb +8 -11
  41. data/lib/sisimai/lhost/qmail.rb +5 -8
  42. data/lib/sisimai/lhost/sendmail.rb +7 -10
  43. data/lib/sisimai/lhost/v5sendmail.rb +15 -17
  44. data/lib/sisimai/lhost/verizon.rb +9 -14
  45. data/lib/sisimai/lhost/x1.rb +4 -6
  46. data/lib/sisimai/lhost/x2.rb +5 -7
  47. data/lib/sisimai/lhost/x3.rb +3 -4
  48. data/lib/sisimai/lhost/x6.rb +4 -6
  49. data/lib/sisimai/lhost/zoho.rb +6 -8
  50. data/lib/sisimai/lhost.rb +1 -1
  51. data/lib/sisimai/mail/mbox.rb +1 -1
  52. data/lib/sisimai/mail/memory.rb +1 -1
  53. data/lib/sisimai/mail.rb +8 -8
  54. data/lib/sisimai/message.rb +11 -13
  55. data/lib/sisimai/order.rb +12 -11
  56. data/lib/sisimai/reason/authfailure.rb +10 -10
  57. data/lib/sisimai/reason/badreputation.rb +4 -6
  58. data/lib/sisimai/reason/blocked.rb +6 -8
  59. data/lib/sisimai/reason/contenterror.rb +5 -6
  60. data/lib/sisimai/reason/delivered.rb +2 -2
  61. data/lib/sisimai/reason/exceedlimit.rb +7 -8
  62. data/lib/sisimai/reason/expired.rb +6 -7
  63. data/lib/sisimai/reason/failedstarttls.rb +5 -7
  64. data/lib/sisimai/reason/feedback.rb +2 -2
  65. data/lib/sisimai/reason/filtered.rb +7 -10
  66. data/lib/sisimai/reason/hasmoved.rb +4 -5
  67. data/lib/sisimai/reason/hostunknown.rb +6 -7
  68. data/lib/sisimai/reason/mailboxfull.rb +7 -8
  69. data/lib/sisimai/reason/mailererror.rb +5 -8
  70. data/lib/sisimai/reason/mesgtoobig.rb +5 -6
  71. data/lib/sisimai/reason/networkerror.rb +5 -8
  72. data/lib/sisimai/reason/norelaying.rb +4 -5
  73. data/lib/sisimai/reason/notaccept.rb +5 -8
  74. data/lib/sisimai/reason/notcompliantrfc.rb +5 -6
  75. data/lib/sisimai/reason/onhold.rb +6 -9
  76. data/lib/sisimai/reason/policyviolation.rb +6 -9
  77. data/lib/sisimai/reason/rejected.rb +5 -6
  78. data/lib/sisimai/reason/requireptr.rb +6 -7
  79. data/lib/sisimai/reason/securityerror.rb +6 -9
  80. data/lib/sisimai/reason/spamdetected.rb +8 -9
  81. data/lib/sisimai/reason/speeding.rb +6 -7
  82. data/lib/sisimai/reason/suppressed.rb +3 -7
  83. data/lib/sisimai/reason/suspend.rb +5 -7
  84. data/lib/sisimai/reason/syntaxerror.rb +3 -5
  85. data/lib/sisimai/reason/systemerror.rb +6 -9
  86. data/lib/sisimai/reason/systemfull.rb +5 -8
  87. data/lib/sisimai/reason/toomanyconn.rb +5 -6
  88. data/lib/sisimai/reason/undefined.rb +2 -2
  89. data/lib/sisimai/reason/userunknown.rb +8 -9
  90. data/lib/sisimai/reason/vacation.rb +4 -5
  91. data/lib/sisimai/reason/virusdetected.rb +4 -5
  92. data/lib/sisimai/reason.rb +13 -13
  93. data/lib/sisimai/rfc1123.rb +4 -8
  94. data/lib/sisimai/rfc1894.rb +5 -6
  95. data/lib/sisimai/rfc2045.rb +27 -31
  96. data/lib/sisimai/rfc3464/thirdparty.rb +1 -1
  97. data/lib/sisimai/rfc3464.rb +7 -9
  98. data/lib/sisimai/rfc3834.rb +5 -9
  99. data/lib/sisimai/rfc5322.rb +8 -26
  100. data/lib/sisimai/rfc791.rb +6 -4
  101. data/lib/sisimai/rhost/google.rb +8 -0
  102. data/lib/sisimai/rhost/microsoft.rb +17 -5
  103. data/lib/sisimai/rhost.rb +2 -2
  104. data/lib/sisimai/smtp/command.rb +1 -1
  105. data/lib/sisimai/smtp/failure.rb +5 -12
  106. data/lib/sisimai/smtp/reply.rb +33 -12
  107. data/lib/sisimai/smtp/status.rb +21 -22
  108. data/lib/sisimai/smtp/transcript.rb +1 -10
  109. data/lib/sisimai/string.rb +20 -30
  110. data/lib/sisimai/version.rb +1 -1
  111. data/lib/sisimai.rb +11 -11
  112. data/set-of-emails/maildir/bsd/rhost-microsoft-06.eml +45 -0
  113. metadata +8 -10
@@ -18,6 +18,7 @@ module Sisimai
18
18
  end
19
19
  return true
20
20
  end
21
+
21
22
  # Find an IPv4 address from the given string
22
23
  # @param [String] argv1 String including an IPv4 address
23
24
  # @return [Array] List of IPv4 addresses
@@ -26,14 +27,15 @@ module Sisimai
26
27
  return nil if argv0.to_s.empty?
27
28
  return [] if argv0.size < 7
28
29
 
30
+ given = argv0.dup
29
31
  ipv4a = []
30
32
  %w|( ) [ ] ,|.each do |e|
31
33
  # Rewrite: "mx.example.jp[192.0.2.1]" => "mx.example.jp 192.0.2.1"
32
- p0 = argv0.index(e); next unless p0
33
- argv0[p0, 1] = ' '
34
+ p0 = given.index(e); next unless p0
35
+ given[p0, 1] = ' '
34
36
  end
35
37
 
36
- argv0.split(' ').each do |e|
38
+ given.split(' ').each do |e|
37
39
  # Find string including an IPv4 address
38
40
  next unless e.index('.') # IPv4 address must include "." character
39
41
 
@@ -55,7 +57,7 @@ module Sisimai
55
57
  eo = ''
56
58
  next
57
59
  end
58
- eo << as.chr
60
+ eo += as.chr
59
61
  break if eo.to_i > 255
60
62
  end
61
63
  ipv4a << e if eo.size > 0 && eo.to_i < 256
@@ -82,7 +82,15 @@ module Sisimai
82
82
  # - 421 4.7.32 Your email has been rate limited because the From: header (RFC5322) in
83
83
  # this message isn't aligned with either the authenticated SPF or DKIM organizational
84
84
  # domain.
85
+ # - 421 5.7.32 Your email was blocked because the From: header (RFC5322) in this message
86
+ # isn't aligned with either the authenticated SPF or DKIM organizational domain.
85
87
  ['421', '4.7.32', 'aligned with either the authenticated spf or dkim'],
88
+ ["421", "5.7.32", "aligned with either the authenticated spf or dkim"],
89
+
90
+ # - 421 4.7.40 Your email has been rate limited because the sending domain doesn't
91
+ # have a DMARC record, or the DMARC record doesn’t specify a DMARC policy. Gmail
92
+ # requires all bulk email senders to add a DMARC record to their sending domain.
93
+ ["421", "4.7.40", "to add a dmarc record to "],
86
94
  ],
87
95
  'badreputation' => [
88
96
  # - 421 4.7.0 This message is suspicious due to the very low reputation of the sending
@@ -23,6 +23,13 @@ module Sisimai
23
23
  # - Access denied, sending domain [$SenderDomain] does not pass DMARC verification
24
24
  # - The sender's domain in the 5322.From address doesn't pass DMARC.
25
25
  ['5.7.509', 0, 0, 'does not pass dmarc verification'],
26
+
27
+ # - 550 5.7.515 Access denied, sending domain EXAMPLE.JP doesn't meet the required
28
+ # authentication level. The sender's domain in the 5322.From address doesn't meet
29
+ # the authentication requirements defined for the sender. To learn how to fix this
30
+ # see: https://go.microsoft.com/fwlink/p/?linkid=2319303
31
+ # Spf= Fail , Dkim= Pass , DMARC= Pass ...
32
+ ["5.7.515", 0, 0, "doesn't meet the required authentication level"],
26
33
  ],
27
34
  'badreputation' => [
28
35
  # Undocumented error messages ---------------------------------------------------------
@@ -597,11 +604,16 @@ module Sisimai
597
604
  ['5.2.14', 0, 0, 'misconfigured forwarding address'],
598
605
 
599
606
  # Undocumented error messages ---------------------------------------------------------
600
- ['4.4.3', 0, 0, 'temporary server error. please try again later attr18'],
601
- ['4.7.0', 0, 0, 'temporary server error. please try again later. prx4 nexthop:'],
602
- ['4.4.24', 0, 0, 'message failed to be replicated: insufficient system resource:'],
603
- ['4.4.25', 0, 0, 'message failed to be replicated: no healthy secondary server available to accept replica at this time.'],
604
- ['4.4.28', 0, 0, 'message failed to be replicated: the operation was canceled'],
607
+ # - 451 4.4.22 Message failed to be replicated: no healthy peers found ... (in reply to end of DATA command)
608
+ # - 451 4.4.23 Message failed to be replicated: No healthy secondary server available
609
+ # to accept replica at this time. ... (in reply to end of DATA command)
610
+ # - 451 4.4.28 Message failed to be replicated:
611
+ # Microsoft.Exchange.Transport.Net.Http.TransportHttpException(session Id: -1) ...(in reply to end of DATA command)
612
+ # - 451 4.4.28 Message failed to be replicated:
613
+ # System.Net.Http.HttpRequestException(session Id: ****) ... (in reply to end of DATA command)
614
+ ["4.4.", "22", "28", "message failed to be replicated:"],
615
+ ["4.4.3", "", "", "temporary server error. please try again later attr18"],
616
+ ["4.7.0", "", "", "temporary server error. please try again later. prx4 nexthop:"],
605
617
 
606
618
  # 550 5.4.318 Message expired, connection reset (SuspiciousRemoteServerError)
607
619
  # 450 4.4.318 Connection was closed abruptly (SuspiciousRemoteServerError)
data/lib/sisimai/rhost.rb CHANGED
@@ -74,8 +74,8 @@ module Sisimai
74
74
  return "" if argvs.nil?
75
75
 
76
76
  rhostclass = name(argvs); return "" if rhostclass.empty?
77
- modulepath = "sisimai/rhost/" << rhostclass.downcase; require modulepath
78
- modulename = "Sisimai::Rhost::" << rhostclass
77
+ modulepath = "sisimai/rhost/#{rhostclass.downcase}"; require modulepath
78
+ modulename = "Sisimai::Rhost::#{rhostclass}"
79
79
 
80
80
  #rhostclass = "sisimai/rhost/" << modulename.downcase.split("::")[2]; require rhostclass
81
81
  reasontext = Module.const_get(modulename).find(argvs)
@@ -31,7 +31,7 @@ module Sisimai
31
31
  return "" unless Sisimai::SMTP::Command.test(argv0)
32
32
 
33
33
  issuedcode = " " + argv0.downcase + " "
34
- commandmap = { "STAR" => "STARTTLS", "XFOR" => "XFORWARD" }
34
+ commandmap = {"STAR" => "STARTTLS", "XFOR" => "XFORWARD"}
35
35
  commandset = []
36
36
 
37
37
  Detectable.each do |e|
@@ -12,8 +12,7 @@ module Sisimai
12
12
  # false: Is not a permanent error
13
13
  # @since v4.17.3
14
14
  def is_permanent(argv1 = '')
15
- return false unless argv1
16
- return false unless argv1.size > 0
15
+ return false if argv1.to_s == ""
17
16
 
18
17
  statuscode = Sisimai::SMTP::Status.find(argv1)
19
18
  statuscode = Sisimai::SMTP::Reply.find(argv1) if statuscode.empty?
@@ -28,16 +27,14 @@ module Sisimai
28
27
  # false: is not a temporary error
29
28
  # @since v5.2.0
30
29
  def is_temporary(argv1 = '')
31
- return false unless argv1
32
- return false unless argv1.size > 0
30
+ return false if argv1.to_s == ""
33
31
 
34
32
  statuscode = Sisimai::SMTP::Status.find(argv1);
35
33
  statuscode = Sisimai::SMTP::Reply.find(argv1) if statuscode.empty?
36
34
  issuedcode = argv1.downcase
37
35
 
38
36
  return true if statuscode[0, 1] == "4"
39
- return true if issuedcode.include?(' temporar')
40
- return true if issuedcode.include?(' persistent')
37
+ return true if issuedcode.include?(' temporar') || issuedcode.include?(' persistent')
41
38
  return false
42
39
  end
43
40
 
@@ -46,9 +43,7 @@ module Sisimai
46
43
  # @param [String] argv2 String including SMTP Status code
47
44
  # @return [Boolean] true: is a hard bounce
48
45
  def is_hardbounce(argv1 = '', argv2 = '')
49
- return false unless argv1
50
- return false unless argv1.size > 0
51
-
46
+ return false if argv1.to_s == ""
52
47
  return false if argv1 == "undefined" || argv1 == "onhold"
53
48
  return false if argv1 == "delivered" || argv1 == "feedback" || argv1 == "vacation"
54
49
  return true if argv1 == "hasmoved" || argv1 == "userunknown" || argv1 == "hostunknown"
@@ -76,9 +71,7 @@ module Sisimai
76
71
  # @param [String] argv2 String including SMTP Status code
77
72
  # @return [Boolean] true: is a soft bounce
78
73
  def is_softbounce(argv1 = '', argv2 = '')
79
- return false unless argv1
80
- return false unless argv1.size > 0
81
-
74
+ return false if argv1.to_s == ""
82
75
  return false if argv1 == "delivered" || argv1 == "feedback" || argv1 == "vacation"
83
76
  return false if argv1 == "hasmoved" || argv1 == "userunknown" || argv1 == "hostunknown"
84
77
  return true if argv1 == "undefined" || argv1 == "onhold"
@@ -58,8 +58,11 @@ module Sisimai
58
58
  # 251 User not local; will forward to <forward-path> (See Section 3.4)
59
59
  # 252 Cannot VRFY user, but will accept message and attempt delivery (See Section 3.5.3)
60
60
  # 253 OK, <n> pending messages for node <domain> started (See RFC1985)
61
+ # 334 A server challenge is sent as a 334 reply with the text part containing the [BASE64]
62
+ # encoded string supplied by the SASL mechanism. This challenge MUST NOT contain any
63
+ # text other than the BASE64 encoded challenge. (RFC4954)
61
64
  # 354 Start mail input; end with <CRLF>.<CRLF>
62
- '211', '214', '220', '221', '235', '250', '251', '252', '253', '354'
65
+ '211', '214', '220', '221', '235', '250', '251', '252', '253', '334', '354'
63
66
  ].freeze
64
67
  ReplyCode4 = [
65
68
  # 421 <domain> Service not available, closing transmission channel (This may be a reply
@@ -86,7 +89,6 @@ module Sisimai
86
89
  # 502 Command not implemented (see Section 4.2.4)
87
90
  # 503 Bad sequence of commands
88
91
  # 504 Command parameter not implemented
89
- # 520 Please use the correct QHLO ID (See https://datatracker.ietf.org/doc/id/draft-fanf-smtp-quickstart-01.txt)
90
92
  # 521 Host does not accept mail (See RFC7504)
91
93
  # 523 Encryption Needed (See RFC5248)
92
94
  # 524 (See RFC5248)
@@ -104,11 +106,27 @@ module Sisimai
104
106
  # 554 Transaction failed (Or, in the case of a connection-opening response, "No SMTP service here")
105
107
  # 555 MAIL FROM/RCPT TO parameters not recognized or not implemented
106
108
  # 556 Domain does not accept mail (See RFC7504)
107
- # 557 draft-moore-email-addrquery-01
108
- '550', '552', '553', '551', '521', '525', '502', '520', '523', '524', '530', '533', '534',
109
- '535', '538', '551', '555', '556', '554', '557', '500', '501', '502', '503', '504',
109
+ "550", "552", "553", "551", "521", "525", "523", "524", "530", "533", "534", "535", "538",
110
+ "555", "556", "554", "500", "501", "502", "503", "504",
110
111
  ].freeze
111
- CodeOfSMTP = { '2' => ReplyCode2, '4' => ReplyCode4, '5' => ReplyCode5 }.freeze
112
+ CodeOfSMTP = {'2' => ReplyCode2, '4' => ReplyCode4, '5' => ReplyCode5}.freeze
113
+ Associated = {
114
+ "422" => ["AUTH", "4.7.12", "securityerror"], # RFC5238
115
+ "432" => ["AUTH", "4.7.12", "securityerror"], # RFC4954, RFC5321
116
+ "500" => ["", "", "syntaxerror"], # RFC5321
117
+ "501" => ["", "", "syntaxerror"], # RFC5321
118
+ "502" => ["", "", "syntaxerror"], # RFC5321
119
+ "503" => ["", "", "syntaxerror"], # RFC5321
120
+ "504" => ["", "", "syntaxerror"], # RFC5321
121
+ "521" => ["CONN", "", "notaccept"], # RFC7504
122
+ "523" => ["AUTH", "", "securityerror"], # RFC5248
123
+ "524" => ["AUTH", "", "securityerror"], # RFC5248
124
+ "525" => ["AUTH", "", "securityerror"], # RFC5248
125
+ "534" => ["AUTH", "5.7.9", "securityerror"], # RFC4954, RFC5248
126
+ "535" => ["AUTH", "5.7.8", "securityerror"], # RFC4954, RFC5248
127
+ "538" => ["AUTH", "5.7.11", "securityerror"], # RFC4954, RFC5248
128
+ "556" => ["RCPT", "", "notaccept"], # RFC7504
129
+ }.freeze
112
130
 
113
131
  # Check whether a reply code is a valid code or not
114
132
  # @param [String] argv1 Reply Code(DSN)
@@ -122,7 +140,7 @@ module Sisimai
122
140
  first = (reply / 100).to_i
123
141
 
124
142
  return false if reply < 211
125
- return false if reply > 557
143
+ return false if reply > 556
126
144
  return false if reply % 100 > 59
127
145
 
128
146
  if first == 2
@@ -135,7 +153,7 @@ module Sisimai
135
153
 
136
154
  if first == 3
137
155
  # 3yz
138
- return false unless reply == 354
156
+ return false unless reply == 334 || reply == 354
139
157
  return true
140
158
  end
141
159
 
@@ -145,11 +163,9 @@ module Sisimai
145
163
  # Get SMTP Reply Code from the given string
146
164
  # @param [String] argv1 String including SMTP Reply Code like 550
147
165
  # @param [String] argv2 Status code like 5.1.1 or 2 or 4 or 5
148
- # @return [String] SMTP Reply Code
149
- # [Nil] The first argument did not include SMTP Reply Code value
166
+ # @return [String] SMTP Reply Code or an empty string
150
167
  def find(argv1 = '', argv2 = '0')
151
- return "" if argv1.to_s.size < 3
152
- return "" if argv1.upcase.include?('X-UNIX')
168
+ return "" if argv1.to_s.size < 3 || argv1.upcase.include?('X-UNIX')
153
169
 
154
170
  esmtperror = ' ' + argv1 + ' '
155
171
  esmtpreply = ''
@@ -182,6 +198,11 @@ module Sisimai
182
198
  return esmtpreply
183
199
  end
184
200
 
201
+ # associatedwith returns a slice associated with the SMTP reply code of the argument
202
+ # @param [String] argv1 SMTP reply code like 550
203
+ # @return [Array] ["SMTP Command", "DSN", "Reason"]
204
+ # @since v5.2.2
205
+ def associatedwith(argv1 = ''); return Associated[argv1] || []; end
185
206
  end
186
207
  end
187
208
  end
@@ -677,11 +677,10 @@ module Sisimai
677
677
  # @param [String] argv1 Reason name
678
678
  # @param [True,False] argv2 false: Permanent error
679
679
  # true: Temporary error
680
- # @return [String, Nil] DSN or Nil if the 1st argument is missing
680
+ # @return [String] DSN or an empty string if the 1st argument is missing
681
681
  # @see name
682
682
  def code(argv1 = nil, argv2 = false)
683
- return nil unless argv1
684
- return nil if argv1.empty?
683
+ return "" if argv1.to_s.empty?
685
684
 
686
685
  table = argv2 ? InternalCode[:temporary] : InternalCode[:permanent]
687
686
  code0 = table[argv1] || InternalCode[:permanent][argv1] || nil
@@ -690,13 +689,11 @@ module Sisimai
690
689
 
691
690
  # Convert from the status code to the reason string
692
691
  # @param [String] argv1 Status code(DSN)
693
- # @return [String] Reason name
694
- # [Nil] The first argument did not match with values in reason list
692
+ # @return [String] Reason name or an empty string
695
693
  # @see code
696
694
  def name(argv1 = nil)
697
- return nil unless argv1
698
- return nil unless Sisimai::SMTP::Status.test(argv1)
699
- return StandardCode[argv1] || nil
695
+ return "" unless Sisimai::SMTP::Status.test(argv1.to_s)
696
+ return StandardCode[argv1] || ""
700
697
  end
701
698
 
702
699
  # Check whether a status code is a valid code or not
@@ -705,29 +702,23 @@ module Sisimai
705
702
  # @see code
706
703
  # @since v5.0.0
707
704
  def test(argv1 = '')
708
- return false if argv1.to_s.empty?
709
- return false if argv1.size < 5
710
- return false if argv1.size > 7
705
+ return false if argv1.to_s.empty? || argv1.size < 5 || argv1.size > 7
711
706
 
712
707
  token = []
713
708
  argv1.split('.').each { |e| token << e.to_i }
714
709
  return false unless token.size == 3
715
- return false if token[0] < 2
716
- return false if token[0] == 3
717
- return false if token[0] > 5
718
- return false if token[1] < 0
719
- return false if token[1] > 7
720
- return false if token[2] < 0
710
+ return false if token[0] < 2 || token[0] == 3 || token[0] > 5
711
+ return false if token[1] < 0 || token[1] > 7
712
+ return false if token[2] < 0
721
713
  return true
722
714
  end
723
715
 
724
716
  # Get a DSN code value from given string including DSN
725
717
  # @param [String] argv1 String including DSN
726
718
  # @param [String] argv2 An SMTP Reply Code or 2 or 4 or 5
727
- # @return [String, Nil] An SMTP Status Code
719
+ # @return [String] An SMTP Status Code or an empty string
728
720
  def find(argv1 = nil, argv2 = '0')
729
- return "" if argv1.to_s.empty?
730
- return "" if argv1.size < 7
721
+ return "" if argv1.to_s.empty? || argv1.size < 7
731
722
 
732
723
  givenclass = argv2[0, 1]
733
724
  eestatuses = if givenclass == '2' || givenclass == '4' || givenclass == '5'
@@ -856,8 +847,8 @@ module Sisimai
856
847
  end
857
848
  return statuscode if statuscode == codeinmesg
858
849
 
859
- zeroindex1 = { 'field' => statuscode.index('.0') || -1, 'error' => codeinmesg.index('.0') || -1 }
860
- zeroindex2 = { 'field' => statuscode.index('.0.0') || -1, 'error' => codeinmesg.index('.0.0') || -1 }
850
+ zeroindex1 = {'field' => statuscode.index('.0') || -1, 'error' => codeinmesg.index('.0') || -1}
851
+ zeroindex2 = {'field' => statuscode.index('.0.0') || -1, 'error' => codeinmesg.index('.0.0') || -1}
861
852
 
862
853
  if zeroindex2['field'] > 0
863
854
  # "Status:" field is "X.0.0"
@@ -892,6 +883,14 @@ module Sisimai
892
883
  return statuscode
893
884
  end
894
885
 
886
+ # is_explicit() returns 0 when the argument is empty or is an internal code
887
+ # @param string argv1 Status code
888
+ # @return bool false: The delivery status code is not explicit
889
+ def is_explicit(argv1 = '')
890
+ return false if argv1.nil? || argv1.empty?
891
+ return false if argv1.size == 7 && argv1.start_with?("5.0.9", "4.0.9")
892
+ return true
893
+ end
895
894
  end
896
895
  end
897
896
  end
@@ -97,10 +97,8 @@ module Sisimai
97
97
  else
98
98
  # SMTP server sent a response "<<< response text"
99
99
  p = e.index('<<< '); next unless p == 0
100
+ e = e[4, e.size].sub(/\A<<<[ ]/, '')
100
101
 
101
- e = e[4, e.size]
102
-
103
- e.sub!(/\A<<<[ ]/, '')
104
102
  if cv = e.match(/\A([2-5]\d\d)[ ]/) then cx['response']['reply'] = cv[1] end
105
103
  if cv = e.match(/\A[245]\d\d[ ]([245][.]\d{1,3}[.]\d{1,3})[ ]/) then cx['response']['status'] = cv[1] end
106
104
  cx['response']['text'] << e
@@ -110,13 +108,6 @@ module Sisimai
110
108
  return nil if esmtp.size == 0
111
109
  return esmtp
112
110
  end
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
111
  end
121
112
  end
122
113
  end
@@ -11,31 +11,26 @@ module Sisimai
11
11
  # @param [String] addr1 Sender address
12
12
  # @param [String] addr2 Recipient address
13
13
  # @param [Integer] epoch Machine time of the email bounce
14
- # @return [String] Message token(MD5 hex digest)
15
- # @return [String] Blank/failed to create token
14
+ # @return [String] Message token(MD5 hex digest) or blank(failed to create token)
16
15
  # @see http://en.wikipedia.org/wiki/ASCII
17
16
  def token(addr1, addr2, epoch)
18
- return nil unless addr1.is_a?(::String)
19
- return nil unless addr2.is_a?(::String)
20
- return nil unless epoch.is_a?(Integer)
21
- return nil if addr1.empty?
22
- return nil if addr2.empty?
17
+ return "" unless addr1.is_a?(::String)
18
+ return "" unless addr2.is_a?(::String)
19
+ return "" unless epoch.is_a?(Integer)
20
+ return "" if addr1.empty? || addr2.empty?
23
21
 
24
22
  # Format: STX(0x02) Sender-Address RS(0x1e) Recipient-Address ETX(0x03)
25
23
  require 'digest/sha1'
26
- return Digest::SHA1.hexdigest(
27
- sprintf("\x02%s\x1e%s\x1e%d\x03", addr1.downcase, addr2.downcase, epoch)
28
- )
24
+ return Digest::SHA1.hexdigest(sprintf("\x02%s\x1e%s\x1e%d\x03", addr1.downcase, addr2.downcase, epoch))
29
25
  end
30
26
 
31
27
  # The argument is 8-bit text or not
32
28
  # @param [String] argvs Any string to be checked
33
- # @return [True,False] false: ASCII Characters only
34
- # true: Including 8-bit character
29
+ # @return [Boolean] false: ASCII Characters only, true: Including 8-bit character
35
30
  def is_8bit(argvs)
36
31
  v = argvs.to_s
37
- return nil if v.empty?
38
- return true unless v =~ /\A[\x00-\x7f]*\z/
32
+ return false if v.empty?
33
+ return true unless v =~ /\A[\x00-\x7f]*\z/
39
34
  return false
40
35
  end
41
36
 
@@ -54,12 +49,12 @@ module Sisimai
54
49
  # Check if each element of the 2nd argument is aligned in the 1st argument or not
55
50
  # @param [String] argv1 String to be checked
56
51
  # @param [Array] argv2 List including the ordered strings
57
- # @return [Bool] 0, 1
52
+ # @return [Boolean]
58
53
  # @since v5.0.0
59
54
  def aligned(argv1, argv2)
60
- return nil if argv1.to_s.empty?
61
- return nil unless argv2.is_a? Array
62
- return nil unless argv2.size > 1
55
+ return false if argv1.to_s.empty?
56
+ return false unless argv2.is_a? Array
57
+ return false unless argv2.size > 1
63
58
 
64
59
  align = -1
65
60
  right = 0
@@ -80,7 +75,7 @@ module Sisimai
80
75
  # @param [Boolean] loose Loose check flag
81
76
  # @return [String] Plain text
82
77
  def to_plain(argv1 = '', loose = false)
83
- return nil if argv1.empty?
78
+ return "" if argv1.empty?
84
79
 
85
80
  plain = argv1
86
81
  if loose || plain =~ Match[:html] || plain =~ Match[:body]
@@ -89,20 +84,15 @@ module Sisimai
89
84
  # 3. <a href = 'http://...'>...</a> to " http://... "
90
85
  # 4. <a href = 'mailto:...'>...</a> to " Value <mailto:...> "
91
86
  plain.scrub!('?')
92
- plain.gsub!(%r|<head>.+</head>|im, '')
93
- plain.gsub!(%r|<style.+?>.+</style>|im, '')
94
- plain.gsub!(%r|<a\s+href\s*=\s*['"](https?://.+?)['"].*?>(.*?)</a>|i, '[\2](\1)')
95
- plain.gsub!(%r|<a\s+href\s*=\s*["']mailto:([^\s]+?)["']>(.*?)</a>|i, '[\2](mailto:\1)')
96
-
87
+ plain = plain.gsub(%r|<head>.+</head>|im, '')
88
+ plain = plain.gsub(%r|<style.+?>.+</style>|im, '')
89
+ plain = plain.gsub(%r|<a\s+href\s*=\s*['"](https?://.+?)['"].*?>(.*?)</a>|i, '[\2](\1)')
90
+ plain = plain.gsub(%r|<a\s+href\s*=\s*["']mailto:([^\s]+?)["']>(.*?)</a>|i, '[\2](mailto:\1)')
97
91
  plain = plain.gsub(/<[^<@>]+?>\s*/, ' ') # Delete HTML tags except <neko@example.jp>
98
92
  plain = plain.gsub(/&lt;/, '<').gsub(/&gt;/, '>') # Convert to angle brackets
99
93
  plain = plain.gsub(/&amp;/, '&').gsub(/&nbsp;/, ' ') # Convert to "&"
100
94
  plain = plain.gsub(/&quot;/, '"').gsub(/&apos;/, "'") # Convert to " and '
101
-
102
- if argv1.size > plain.size
103
- plain = plain.squeeze(' ')
104
- plain << "\n"
105
- end
95
+ plain = "#{plain.squeeze(' ')}\n" if argv1.size > plain.size
106
96
  end
107
97
 
108
98
  return plain
@@ -113,7 +103,7 @@ module Sisimai
113
103
  # @param [String] argv2 Encoding name before converting
114
104
  # @return [String] UTF-8 Encoded string
115
105
  def to_utf8(argv1 = '', argv2 = nil)
116
- return nil if argv1.empty?
106
+ return "" if argv1.empty?
117
107
 
118
108
  encodefrom = argv2 || false
119
109
  getencoded = ''
@@ -1,4 +1,4 @@
1
1
  # Define the version number of Sisimai
2
2
  module Sisimai
3
- VERSION = '5.2.1'.freeze
3
+ VERSION = '5.4.0'.freeze
4
4
  end
data/lib/sisimai.rb CHANGED
@@ -1,6 +1,6 @@
1
- # Sisimai is a library that decodes complex and diverse bounce emails and outputs the results of
2
- # the delivery failure, such as the reason for the bounce and the recipient email address, in
3
- # structured data. It is also possible to output in JSON format.
1
+ # Sisimai (pronounced /ɕi.ɕi.ma.i/) is a library that decodes complex and diverse bounce emails and
2
+ # outputs the results of the delivery failure, such as the reason for the bounce and the recipient
3
+ # email address, in structured data. It is also possible to output in JSON format.
4
4
  require 'sisimai/version'
5
5
  module Sisimai
6
6
  class << self
@@ -30,17 +30,17 @@ module Sisimai
30
30
  while r = mail.data.read do
31
31
  # Read and decode each email file
32
32
  path = mail.data.path
33
- args = { data: r, hook: c___[0], origin: path, delivered: argv1[:delivered], vacation: argv1[:vacation] }
33
+ args = {data: r, hook: c___[0], origin: path, delivered: argv1[:delivered], vacation: argv1[:vacation]}
34
34
  fact = Sisimai::Fact.rise(**args) || []
35
35
 
36
36
  if c___[1]
37
37
  # Run the callback function specified with "c___" parameter of Sisimai.rise after reading
38
38
  # each email file in Maildir/ every time
39
- args = { 'kind' => kind, 'mail' => r, 'path' => path, 'fact' => fact }
39
+ args = {'kind' => kind, 'mail' => r, 'path' => path, 'fact' => fact}
40
40
  begin
41
41
  c___[1].call(args) if c___[1].is_a?(Proc)
42
42
  rescue StandardError => ce
43
- warn ' ***warning: Something is wrong in the second element of the ":c___":' << ce.to_s
43
+ warn ' ***warning: Something is wrong in the second element of the ":c___":' + ce.to_s
44
44
  end
45
45
  end
46
46
 
@@ -61,7 +61,7 @@ module Sisimai
61
61
  # @options argv1 [Lambda] hook Lambda object to be called back
62
62
  # @return [String] Decoded data as JSON text
63
63
  def dump(argv0, **argv1)
64
- return nil unless argv0
64
+ return "" unless argv0
65
65
  nyaan = Sisimai.rise(argv0, **argv1) || []
66
66
 
67
67
  if RUBY_PLATFORM.start_with?('java')
@@ -81,14 +81,14 @@ module Sisimai
81
81
  table = {}
82
82
 
83
83
  %w[Lhost ARF RFC3464 RFC3834].each do |e|
84
- r = 'Sisimai::' << e
84
+ r = "Sisimai::#{e}"
85
85
  require r.gsub('::', '/').downcase
86
86
 
87
87
  if e == 'Lhost'
88
88
  # Sisimai::Lhost::*
89
89
  Module.const_get(r).send(:index).each do |ee|
90
90
  # Load and get the value of "description" from each module
91
- rr = 'Sisimai::' << e + '::' << ee
91
+ rr = "Sisimai::#{e}::#{ee}"
92
92
  require rr.gsub('::', '/').downcase
93
93
  table[rr.to_sym] = Module.const_get(rr).send(:description)
94
94
  end
@@ -112,7 +112,7 @@ module Sisimai
112
112
  names += %w[Delivered Feedback Undefined Vacation]
113
113
  while e = names.shift do
114
114
  # Call .description() method of Sisimai::Reason::*
115
- r = 'Sisimai::Reason::' << e
115
+ r = "Sisimai::Reason::#{e}"
116
116
  require r.gsub('::', '/').downcase
117
117
  table[e.to_sym] = Module.const_get(r).send(:description)
118
118
  end
@@ -124,7 +124,7 @@ module Sisimai
124
124
  # @param [String] Error message text
125
125
  # @return [String] Reason text
126
126
  def match(argvs = '')
127
- return nil if argvs.empty?
127
+ return "" if argvs.empty?
128
128
  require 'sisimai/reason'
129
129
  return Sisimai::Reason.match(argvs.downcase)
130
130
  end
@@ -0,0 +1,45 @@
1
+ Return-Path: <>
2
+ Delivered-To: neko@e.nyaaaan.example.onmicrosoft.com
3
+ Received: from relay.nyaaaan.example.onmicrosoft.com (unknown [192.0.2.25])
4
+ by ee.nyaaaan.example.onmicrosoft.com (Postfix) with ESMTPS id JFzCqYfyTkzvHbZW
5
+ for <neko@e.nyaaaan.example.onmicrosoft.com>; Sun, 25 May 2025 22:22:22 +0900 (JST)
6
+ Received: from cat.example.co.jp (ip-192-0-2-225.us-west-1.compute.internal [192.0.2.225])
7
+ by relay.nyaaaan.example.onmicrosoft.com (Postfix) with ESMTPS id D09y2f7PVFzmfJf9
8
+ for <kijitora@nyaaaan.example.onmicrosoft.com>; Sun, 25 May 2025 22:22:22 +0900 (JST)
9
+ Message-ID: <20250525222222.00002.nekochan@cat.example.co.jp>
10
+ Date: 25 May 2025 22:22:22 +0900
11
+ From: MAILER-DAEMON@cat.example.co.jp
12
+ To: kijitora@nyaaaan.example.onmicrosoft.com
13
+ Subject: failure notice
14
+
15
+ Hi. This is the qmail-send program at cat.example.co.jp.
16
+ I'm afraid I wasn't able to deliver your message to the following addresses.
17
+ This is a permanent error; I've given up. Sorry it didn't work out.
18
+
19
+ <sabineko@example.onmicrosoft.com>:
20
+ 198.51.100.25 failed after I sent the message.
21
+ Remote host said: 550 5.7.515 Access denied, sending domain NYAAAAN.EXAMPLE.JP doesn't meet the required authentication level. The sender's domain in the 5322.From address doesn't meet the authentication requirements defined for the sender. To learn how to fix this see: https://go.microsoft.com/fwlink/p/?linkid=2319303 Spf= Fail , Dkim= Pass , DMARC= Pass [0000000000000.JPNP255.PROD.OUTLOOK.COM 2025-05-25T22:22:22.000Z 0000000000000EEE] [0000000000000.jpnprd00.prod.outlook.com 2025-05-25T22:22:22.000Z 0000000000000EEE] [0000000000000EE.apcprd00.prod.outlook.com 2025-05-25T22:22:22.000Z 0000000000000EEE]
22
+
23
+ --- Below this line is a copy of the message.
24
+
25
+ Return-Path: <kijitora@nyaaaan.example.onmicrosoft.com>
26
+ Received: (qmail 32768 invoked by uid 99); 25 May 2025 22:22:22 +0900
27
+ Delivered-To: mikeneko@example.org
28
+ Received: (qmail 32767 invoked by uid 99); 25 May 2025 22:22:22 +0900
29
+ Received: from relay-2.nyaaaan.example.onmicrosoft.com (203.0.113.25)
30
+ by cat.example.co.jp with ESMTPS (AES256-GCM-SHA384 encrypted); 25 May 2025 22:22:22 +0900
31
+ Received: from relay-1.nyaaaan.example.onmicrosoft.com (unknown [192.0.2.254])
32
+ by relay-2.nyaaaan.example.onmicrosoft.com (Postfix) with ESMTP id djK8m53Kqpzf8pK9
33
+ for <mikeneko@example.org>; Sun, 25 May 2025 22:22:22 +0900 (JST)
34
+ DKIM-Filter: OpenDKIM Filter v2.11.0 relay-2.nyaaaan.example.onmicrosoft.com djK8m53Kqpzf8pK9
35
+ DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=nyaaaan.example.onmicrosoft.com;
36
+ Content-Type: text/plain; charset=us-ascii
37
+ MIME-Version: 1.0
38
+ From: <kijitora@nyaaaan.example.onmicrosoft.com>
39
+ Date: Sun, 25 May 2025 22:22:22 +0900
40
+ Subject: Nyaaaan?
41
+ Message-Id: <5E82FFFE9D95.88616A92ACCB@relay-1.nyaaaan.example.onmicrosoft.com>
42
+ To: <mikeneko@example.org>
43
+
44
+ Nyaaaan?
45
+