sisimai 5.4.1 → 5.5.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 (146) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/codecovio.yml +1 -1
  3. data/.github/workflows/rake-test.yml +1 -1
  4. data/ChangeLog.md +35 -0
  5. data/Makefile +2 -4
  6. data/README-JA.md +23 -20
  7. data/README.md +23 -20
  8. data/lib/sisimai/address.rb +92 -44
  9. data/lib/sisimai/arf.rb +7 -8
  10. data/lib/sisimai/datetime.rb +2 -2
  11. data/lib/sisimai/fact/json.rb +1 -2
  12. data/lib/sisimai/fact/yaml.rb +1 -2
  13. data/lib/sisimai/fact.rb +60 -35
  14. data/lib/sisimai/lda.rb +2 -2
  15. data/lib/sisimai/lhost/activehunter.rb +4 -5
  16. data/lib/sisimai/lhost/amazonses.rb +3 -4
  17. data/lib/sisimai/lhost/apachejames.rb +2 -2
  18. data/lib/sisimai/lhost/biglobe.rb +6 -6
  19. data/lib/sisimai/lhost/courier.rb +7 -7
  20. data/lib/sisimai/lhost/domino.rb +6 -6
  21. data/lib/sisimai/lhost/dragonfly.rb +5 -5
  22. data/lib/sisimai/lhost/einsundeins.rb +4 -4
  23. data/lib/sisimai/lhost/exchange2003.rb +7 -7
  24. data/lib/sisimai/lhost/exchange2007.rb +3 -3
  25. data/lib/sisimai/lhost/exim.rb +7 -7
  26. data/lib/sisimai/lhost/ezweb.rb +3 -2
  27. data/lib/sisimai/lhost/fml.rb +9 -9
  28. data/lib/sisimai/lhost/gmail.rb +9 -9
  29. data/lib/sisimai/lhost/gmx.rb +3 -3
  30. data/lib/sisimai/lhost/googlegroups.rb +6 -7
  31. data/lib/sisimai/lhost/googleworkspace.rb +5 -6
  32. data/lib/sisimai/lhost/imailserver.rb +4 -4
  33. data/lib/sisimai/lhost/kddi.rb +4 -4
  34. data/lib/sisimai/lhost/mailfoundry.rb +3 -3
  35. data/lib/sisimai/lhost/{mailmarshalsmtp.rb → mailmarshal.rb} +5 -5
  36. data/lib/sisimai/lhost/messagingserver.rb +8 -8
  37. data/lib/sisimai/lhost/mfilter.rb +8 -4
  38. data/lib/sisimai/lhost/mimecast.rb +105 -0
  39. data/lib/sisimai/lhost/notes.rb +5 -5
  40. data/lib/sisimai/lhost/opensmtpd.rb +5 -5
  41. data/lib/sisimai/lhost/postfix.rb +38 -32
  42. data/lib/sisimai/lhost/qmail.rb +6 -6
  43. data/lib/sisimai/lhost/sendmail.rb +13 -13
  44. data/lib/sisimai/lhost/{interscanmss.rb → trendmicro.rb} +8 -9
  45. data/lib/sisimai/lhost/v5sendmail.rb +7 -7
  46. data/lib/sisimai/lhost/verizon.rb +3 -3
  47. data/lib/sisimai/lhost/x1.rb +7 -4
  48. data/lib/sisimai/lhost/x2.rb +40 -12
  49. data/lib/sisimai/lhost/x3.rb +3 -3
  50. data/lib/sisimai/lhost/x6.rb +2 -2
  51. data/lib/sisimai/lhost/zoho.rb +5 -5
  52. data/lib/sisimai/lhost.rb +18 -17
  53. data/lib/sisimai/mail/maildir.rb +4 -4
  54. data/lib/sisimai/mail/mbox.rb +4 -4
  55. data/lib/sisimai/mail/memory.rb +1 -1
  56. data/lib/sisimai/mail/stdin.rb +2 -2
  57. data/lib/sisimai/message.rb +34 -34
  58. data/lib/sisimai/order.rb +5 -4
  59. data/lib/sisimai/reason/authfailure.rb +1 -1
  60. data/lib/sisimai/reason/badreputation.rb +1 -1
  61. data/lib/sisimai/reason/blocked.rb +2 -1
  62. data/lib/sisimai/reason/contenterror.rb +1 -2
  63. data/lib/sisimai/reason/exceedlimit.rb +1 -1
  64. data/lib/sisimai/reason/expired.rb +1 -1
  65. data/lib/sisimai/reason/failedstarttls.rb +1 -1
  66. data/lib/sisimai/reason/filtered.rb +1 -1
  67. data/lib/sisimai/reason/hasmoved.rb +1 -1
  68. data/lib/sisimai/reason/hostunknown.rb +2 -2
  69. data/lib/sisimai/reason/mailboxfull.rb +1 -1
  70. data/lib/sisimai/reason/mailererror.rb +1 -1
  71. data/lib/sisimai/reason/mesgtoobig.rb +1 -1
  72. data/lib/sisimai/reason/networkerror.rb +1 -1
  73. data/lib/sisimai/reason/norelaying.rb +2 -2
  74. data/lib/sisimai/reason/notaccept.rb +2 -3
  75. data/lib/sisimai/reason/notcompliantrfc.rb +1 -1
  76. data/lib/sisimai/reason/policyviolation.rb +2 -4
  77. data/lib/sisimai/reason/rejected.rb +5 -3
  78. data/lib/sisimai/reason/requireptr.rb +1 -1
  79. data/lib/sisimai/reason/securityerror.rb +1 -1
  80. data/lib/sisimai/reason/spamdetected.rb +1 -1
  81. data/lib/sisimai/reason/speeding.rb +1 -1
  82. data/lib/sisimai/reason/suspend.rb +2 -1
  83. data/lib/sisimai/reason/systemerror.rb +1 -1
  84. data/lib/sisimai/reason/systemfull.rb +1 -1
  85. data/lib/sisimai/reason/toomanyconn.rb +1 -1
  86. data/lib/sisimai/reason/userunknown.rb +4 -3
  87. data/lib/sisimai/reason/vacation.rb +1 -1
  88. data/lib/sisimai/reason/virusdetected.rb +1 -1
  89. data/lib/sisimai/reason.rb +12 -12
  90. data/lib/sisimai/rfc1123.rb +58 -18
  91. data/lib/sisimai/rfc1894.rb +6 -8
  92. data/lib/sisimai/rfc2045.rb +25 -13
  93. data/lib/sisimai/rfc3464/thirdparty.rb +2 -3
  94. data/lib/sisimai/rfc3464.rb +6 -6
  95. data/lib/sisimai/rfc3834.rb +18 -8
  96. data/lib/sisimai/rfc5322.rb +9 -9
  97. data/lib/sisimai/rfc791.rb +2 -2
  98. data/lib/sisimai/rhost/aol.rb +4 -1
  99. data/lib/sisimai/rhost/apple.rb +11 -7
  100. data/lib/sisimai/rhost/cox.rb +9 -5
  101. data/lib/sisimai/rhost/facebook.rb +9 -3
  102. data/lib/sisimai/rhost/franceptt.rb +86 -37
  103. data/lib/sisimai/rhost/godaddy.rb +10 -1
  104. data/lib/sisimai/rhost/google.rb +6 -6
  105. data/lib/sisimai/rhost/gsuite.rb +1 -1
  106. data/lib/sisimai/rhost/kddi.rb +1 -1
  107. data/lib/sisimai/rhost/messagelabs.rb +160 -2
  108. data/lib/sisimai/rhost/microsoft.rb +77 -26
  109. data/lib/sisimai/rhost/mimecast.rb +30 -21
  110. data/lib/sisimai/rhost/nttdocomo.rb +69 -89
  111. data/lib/sisimai/rhost/outlook.rb +1 -1
  112. data/lib/sisimai/rhost/spectrum.rb +1 -1
  113. data/lib/sisimai/rhost/tencent.rb +5 -4
  114. data/lib/sisimai/rhost/yahooinc.rb +2 -2
  115. data/lib/sisimai/rhost/zoho.rb +72 -0
  116. data/lib/sisimai/rhost.rb +4 -3
  117. data/lib/sisimai/smtp/command.rb +2 -2
  118. data/lib/sisimai/smtp/reply.rb +11 -4
  119. data/lib/sisimai/smtp/status.rb +17 -8
  120. data/lib/sisimai/smtp/transcript.rb +3 -3
  121. data/lib/sisimai/string.rb +6 -10
  122. data/lib/sisimai/version.rb +1 -1
  123. data/lib/sisimai.rb +1 -1
  124. data/set-of-emails/maildir/bsd/lhost-exim-56.eml +40 -40
  125. data/set-of-emails/maildir/bsd/lhost-mfilter-05.eml +41 -0
  126. data/set-of-emails/maildir/bsd/lhost-mimecast-01.eml +46 -0
  127. data/set-of-emails/maildir/bsd/lhost-mimecast-02.eml +59 -0
  128. data/set-of-emails/maildir/bsd/lhost-postfix-79.eml +81 -0
  129. data/set-of-emails/maildir/bsd/lhost-postfix-80.eml +84 -0
  130. data/set-of-emails/maildir/bsd/lhost-x1-03.eml +26 -0
  131. data/set-of-emails/maildir/bsd/lhost-x1-04.eml +45 -0
  132. data/set-of-emails/maildir/bsd/lhost-x2-07.eml +30 -0
  133. data/set-of-emails/maildir/bsd/rfc3464-66.eml +170 -0
  134. data/set-of-emails/maildir/bsd/rfc3834-06.eml +59 -0
  135. data/set-of-emails/maildir/bsd/rhost-zoho-01.eml +88 -0
  136. data/set-of-emails/maildir/bsd/rhost-zoho-02.eml +86 -0
  137. data/set-of-emails/maildir/bsd/rhost-zoho-03.eml +87 -0
  138. data/set-of-emails/maildir/bsd/rhost-zoho-04.eml +86 -0
  139. data/set-of-emails/maildir/not/rb-issue-368-bug.eml +39 -0
  140. data/sisimai-java.gemspec +1 -1
  141. data/sisimai.gemspec +1 -1
  142. metadata +26 -9
  143. /data/set-of-emails/maildir/bsd/{lhost-mailmarshalsmtp-02.eml → lhost-mailmarshal-02.eml} +0 -0
  144. /data/set-of-emails/maildir/bsd/{lhost-interscanmss-01.eml → lhost-trendmicro-01.eml} +0 -0
  145. /data/set-of-emails/maildir/bsd/{lhost-interscanmss-02.eml → lhost-trendmicro-02.eml} +0 -0
  146. /data/set-of-emails/maildir/bsd/{lhost-interscanmss-03.eml → lhost-trendmicro-03.eml} +0 -0
@@ -19,7 +19,6 @@ module Sisimai
19
19
  Index = [
20
20
  'an illegal attachment on your message',
21
21
  'because the recipient is not accepting mail with ', # AOL Phoenix
22
- 'by non-member to a members-only list',
23
22
  'closed mailing list',
24
23
  'denied by policy',
25
24
  'email not accepted for policy reasons',
@@ -35,9 +34,8 @@ module Sisimai
35
34
  'messages with multiple addresses',
36
35
  'rejected for policy reasons',
37
36
  'protocol violation',
38
- 'the email address used to send your message is not subscribed to this group',
39
37
  'the message was rejected by organization policy',
40
- 'this message was blocked because its content presents a potential',
38
+ 'this message was blocked because its content presents a potential', # https://support.google.com/mail/answer/6590
41
39
  'we do not accept messages containing images or other attachments',
42
40
  "you're using a mass mailer",
43
41
  ].freeze
@@ -53,7 +51,7 @@ module Sisimai
53
51
  # @return [Boolean] false: Did not match, true: Matched
54
52
  # @since 4.22.0
55
53
  def match(argv1)
56
- return false unless argv1
54
+ return false if argv1.nil? || argv1.empty?
57
55
  return true if Index.any? { |a| argv1.include?(a) }
58
56
  return true if Pairs.any? { |a| Sisimai::String.aligned(argv1, a) }
59
57
  return false
@@ -23,11 +23,12 @@ module Sisimai
23
23
  'access denied (sender blacklisted)',
24
24
  'address rejected',
25
25
  'administrative prohibition',
26
- 'batv failed to verify', # SoniWall
27
- 'batv validation failure', # SoniWall
26
+ 'batv failed to verify', # SonicWall
27
+ 'batv validation failure', # SonicWall
28
28
  'backscatter protection detected an invalid or expired email address', # MDaemon
29
29
  "because the sender isn't on the recipient's list of senders to accept mail from",
30
30
  'bogus mail from', # IMail - block empty sender
31
+ 'by non-member to a members-only list',
31
32
  "can't determine purported responsible address",
32
33
  'connections not accepted from servers without a valid sender domain',
33
34
  'denied [bouncedeny]', # McAfee
@@ -66,6 +67,7 @@ module Sisimai
66
67
  'sender was rejected', # qmail
67
68
  'spam reporting address', # SendGrid|a message to an address has previously been marked as Spam by the recipient.
68
69
  'syntax error: empty email address',
70
+ 'the email address used to send your message is not subscribed to this group',
69
71
  'the message has been rejected by batv defense',
70
72
  'this server does not accept mail from',
71
73
  'transaction failed unsigned dsn for',
@@ -83,7 +85,7 @@ module Sisimai
83
85
  # @param [String] argv1 String to be matched with regular expressions
84
86
  # @return [Boolean] false: Did not match, true: Matched
85
87
  def match(argv1)
86
- return false unless argv1
88
+ return false if argv1.nil? || argv1.empty?
87
89
  return false if IsNot.any? { |a| argv1.include?(a) }
88
90
  return true if Index.any? { |a| argv1.include?(a) }
89
91
  return false
@@ -44,7 +44,7 @@ module Sisimai
44
44
  # @param [String] argv1 String to be matched with regular expressions
45
45
  # @return [Boolean] false: Did not match, true: Matched
46
46
  def match(argv1)
47
- return false unless argv1
47
+ return false if argv1.nil? || argv1.empty?
48
48
  return true if Index.any? { |a| argv1.include?(a) }
49
49
  return true if Pairs.any? { |a| Sisimai::String.aligned(argv1, a) }
50
50
  return false
@@ -48,7 +48,7 @@ module Sisimai
48
48
  # @param [String] argv1 String to be matched with regular expressions
49
49
  # @return [Boolean] false: Did not match, true: Matched
50
50
  def match(argv1)
51
- return false unless argv1
51
+ return false if argv1.nil? || argv1.empty?
52
52
  return true if Index.any? { |a| argv1.include?(a) }
53
53
  return true if Pairs.any? { |a| Sisimai::String.aligned(argv1, a) }
54
54
  return false
@@ -122,7 +122,7 @@ module Sisimai
122
122
  # @param [String] argv1 String to be matched with regular expressions
123
123
  # @return [Boolean] false: Did not match, true: Matched
124
124
  def match(argv1)
125
- return false unless argv1
125
+ return false if argv1.nil? || argv1.empty?
126
126
  return true if Index.any? { |a| argv1.include?(a) }
127
127
  return true if Pairs.any? { |a| Sisimai::String.aligned(argv1, a) }
128
128
  return false
@@ -18,7 +18,7 @@ module Sisimai
18
18
  # @param [String] argv1 String to be matched with regular expressions
19
19
  # @return [Boolean] false: Did not match, true: Matched
20
20
  def match(argv1)
21
- return false unless argv1
21
+ return false if argv1.nil? || argv1.empty?
22
22
  return true if Index.any? { |a| argv1.include?(a) }
23
23
  return false
24
24
  end
@@ -22,6 +22,7 @@ module Sisimai
22
22
  'mailbox unavailable or access denied',
23
23
  'recipient rejected: temporarily inactive',
24
24
  'recipient suspend the service',
25
+ "the email account that you tried to reach is inactive",
25
26
  'this account has been disabled or discontinued',
26
27
  'this account has been temporarily suspended',
27
28
  'this address no longer accepts mail',
@@ -38,7 +39,7 @@ module Sisimai
38
39
  # @param [String] argv1 String to be matched with regular expressions
39
40
  # @return [Boolean] false: Did not match, true: Matched
40
41
  def match(argv1)
41
- return false unless argv1
42
+ return false if argv1.nil? || argv1.empty?
42
43
  return true if Index.any? { |a| argv1.include?(a) }
43
44
  return false
44
45
  end
@@ -45,7 +45,7 @@ module Sisimai
45
45
  # @param [String] argv1 String to be matched with regular expressions
46
46
  # @return [Boolean] false: Did not match, true: Matched
47
47
  def match(argv1)
48
- return false unless argv1
48
+ return false if argv1.nil? || argv1.empty?
49
49
  return true if Index.any? { |a| argv1.include?(a) }
50
50
  return true if Pairs.any? { |a| Sisimai::String.aligned(argv1, a) }
51
51
  return false
@@ -20,7 +20,7 @@ module Sisimai
20
20
  # @param [String] argv1 String to be matched with regular expressions
21
21
  # @return [Boolean] false: Did not match, true: Matched
22
22
  def match(argv1)
23
- return false unless argv1
23
+ return false if argv1.nil? || argv1.empty?
24
24
  return true if Index.any? { |a| argv1.include?(a) }
25
25
  return false
26
26
  end
@@ -34,7 +34,7 @@ module Sisimai
34
34
  # @param [String] argv1 String to be matched with regular expressions
35
35
  # @return [Boolean] false: Did not match, true: Matched
36
36
  def match(argv1)
37
- return false unless argv1
37
+ return false if argv1.nil? || argv1.empty?
38
38
  return true if Index.any? { |a| argv1.include?(a) }
39
39
  return false
40
40
  end
@@ -112,6 +112,7 @@ module Sisimai
112
112
  'user unknown',
113
113
  'utilisateur inconnu !',
114
114
  'vdeliver: invalid or unknown virtual user',
115
+ 'weil die adresse nicht gefunden wurde oder keine e-mails empfangen kann',
115
116
  'your envelope recipient is in my badrcptto list',
116
117
  ].freeze
117
118
  Pairs = [
@@ -147,7 +148,7 @@ module Sisimai
147
148
  # @param [String] argv1 String to be matched with regular expressions
148
149
  # @return [Boolean] false: Did not match, true: Matched
149
150
  def match(argv1)
150
- return false unless argv1
151
+ return false if argv1.nil? || argv1.empty?
151
152
  return true if Index.any? { |a| argv1.include?(a) }
152
153
  return true if Pairs.any? { |a| Sisimai::String.aligned(argv1, a) }
153
154
  return false
@@ -183,12 +184,12 @@ module Sisimai
183
184
  next
184
185
  end
185
186
 
186
- next unless r.match(issuedcode)
187
+ next if r.match(issuedcode) == false
187
188
  # Match with reason defined in Sisimai::Reason::* except UserUnknown.
188
189
  matchother = true
189
190
  break
190
191
  end
191
- return true unless matchother # Did not match with other message patterns
192
+ return true if matchother == false # Did not match with other message patterns
192
193
 
193
194
  elsif argvs['command'] == 'RCPT'
194
195
  # When the SMTP command is not "RCPT", the session rejected by other
@@ -18,7 +18,7 @@ module Sisimai
18
18
  # @param [String] argv1 String to be matched with regular expressions
19
19
  # @return [Boolean] false: Did not match, true: Matched
20
20
  def match(argv1)
21
- return false unless argv1
21
+ return false if argv1.nil? || argv1.empty?
22
22
  return true if Index.any? { |a| argv1.include?(a) }
23
23
  return false
24
24
  end
@@ -31,7 +31,7 @@ module Sisimai
31
31
  # @return [Boolean] false: Did not match, true: Matched
32
32
  # @since 4.22.0
33
33
  def match(argv1)
34
- return false unless argv1
34
+ return false if argv1.nil? || argv1.empty?
35
35
  return true if Index.any? { |a| argv1.include?(a) }
36
36
  return false
37
37
  end
@@ -68,11 +68,11 @@ module Sisimai
68
68
  # @return [String] Bounce reason or an empty string if the argument is missing or not Hash
69
69
  # @see anotherone
70
70
  def find(argvs)
71
- return "" unless argvs
72
- unless GetRetried[argvs['reason']]
71
+ return "" if argvs.nil?
72
+ if GetRetried[argvs['reason']].nil?
73
73
  # Return reason text already decided except reason match with the regular expression of
74
74
  # retry() method.
75
- return argvs['reason'] unless argvs['reason'].empty?
75
+ return argvs['reason'] if argvs['reason'].empty? == false
76
76
  end
77
77
  return 'delivered' if argvs['deliverystatus'].start_with?('2.')
78
78
 
@@ -94,7 +94,7 @@ module Sisimai
94
94
  warn " ***warning: Failed to load #{p}"
95
95
  next
96
96
  end
97
- next unless r.true(argvs)
97
+ next if r.true(argvs) == false
98
98
  reasontext = r.text
99
99
  break
100
100
  end
@@ -113,7 +113,7 @@ module Sisimai
113
113
  # Try to match with message patterns in Sisimai::Reason::Vacation
114
114
  require 'sisimai/reason/vacation'
115
115
  reasontext = 'vacation' if Sisimai::Reason::Vacation.match(issuedcode.downcase)
116
- reasontext ||= 'onhold' unless issuedcode.empty?
116
+ reasontext ||= 'onhold' if issuedcode.empty? == false
117
117
  reasontext ||= 'undefined'
118
118
  end
119
119
  end
@@ -125,7 +125,7 @@ module Sisimai
125
125
  # @return [String, Nil] Bounce reason or nli if the argument is missing or not Hash
126
126
  # @see find()
127
127
  def anotherone(argvs)
128
- return argvs['reason'] unless argvs['reason'].empty?
128
+ return argvs['reason'] if argvs['reason'].empty? == false
129
129
 
130
130
  require 'sisimai/smtp/status'
131
131
  issuedcode = argvs['diagnosticcode'].downcase || ''
@@ -150,11 +150,11 @@ module Sisimai
150
150
  next
151
151
  end
152
152
 
153
- next unless r.match(issuedcode)
153
+ next if r.match(issuedcode) == false
154
154
  reasontext = e.downcase
155
155
  break
156
156
  end
157
- break unless reasontext.empty?
157
+ break if reasontext.empty? == false
158
158
 
159
159
  # Check the value of Status:
160
160
  code2digit = statuscode[0, 3] || ''
@@ -175,7 +175,7 @@ module Sisimai
175
175
  require 'sisimai/reason/syntaxerror'
176
176
  reasontext = 'syntaxerror' if Sisimai::Reason::SyntaxError.true(argvs)
177
177
  end
178
- break unless reasontext.empty?
178
+ break if reasontext.empty? == false
179
179
 
180
180
  # Check the value of Action: field, first
181
181
  if actiontext.start_with?('delayed', 'expired')
@@ -195,7 +195,7 @@ module Sisimai
195
195
  # @param [String] argv1 Error message
196
196
  # @return [String] Bounce reason
197
197
  def match(argv1)
198
- return "" unless argv1
198
+ return "" if argv1.nil?
199
199
 
200
200
  reasontext = ''
201
201
  issuedcode = argv1.downcase
@@ -214,11 +214,11 @@ module Sisimai
214
214
  next
215
215
  end
216
216
 
217
- next unless r.match(issuedcode)
217
+ next if r.match(issuedcode) == false
218
218
  reasontext = r.text
219
219
  break
220
220
  end
221
- return reasontext unless reasontext.empty?
221
+ return reasontext if reasontext.empty? == false
222
222
 
223
223
  if issuedcode.upcase.include?('X-UNIX; ')
224
224
  # X-Unix; ...
@@ -3,6 +3,7 @@ module Sisimai
3
3
  module RFC1123
4
4
  class << self
5
5
  require 'sisimai/string'
6
+ require 'sisimai/rfc791'
6
7
  Sandwiched = [
7
8
  # (Postfix) postfix/src/smtp/smtp_proto.c: "host %s said: %s (in reply to %s)",
8
9
  # - <kijitora@example.com>: host re2.example.com[198.51.100.2] said: 550 ...
@@ -37,32 +38,72 @@ module Sisimai
37
38
  # @return [Boolean] false: is not a valid internet host, true: is a valid interneet host
38
39
  # @since v5.2.0
39
40
  def is_internethost(argv0 = '')
40
- return false unless argv0
41
- return false if argv0.size < 4 || argv0.size > 255
41
+ return false if argv0.nil? || argv0.size < 4 || argv0.size > 255
42
+ return true if argv0 == 'localhost' || argv0 == 'localhost6'
42
43
  return false if argv0.include?(".") == false
43
44
  return false if argv0.include?("..") || argv0.include?("--")
44
45
  return false if argv0.start_with?(".", "-") || argv0.end_with?("-")
45
46
 
46
- hostnameok = true
47
47
  characters = argv0.upcase.split("")
48
48
  characters.each do |e|
49
49
  # Check each characater is a number or an alphabet
50
50
  f = e.ord
51
- if f < 45 then hostnameok = false; break; end # 45 = '-'
52
- if f == 47 then hostnameok = false; break; end # 47 = '/'
53
- if f > 57 && f < 65 then hostnameok = false; break; end # 57 = '9', 65 = 'A'
54
- if f > 90 then hostnameok = false; break; end # 90 = 'Z'
51
+ return false if f < 45 # 45 = '-'
52
+ return false if f == 47 # 47 = '/'
53
+ return false if f > 57 && f < 65 # 57 = '9', 65 = 'A'
54
+ return false if f > 90 # 90 = 'Z'
55
55
  end
56
- return false if hostnameok == false
57
56
 
58
57
  p1 = argv0.rindex(".")
59
58
  cv = argv0[p1 + 1, argv0.size - p1]; return false if cv.size > 63
60
59
  cv.split("").each do |e|
61
60
  # The top level domain should not include a number
62
- f = e.ord
63
- if f > 47 && f < 58 then hostnameok = false; break; end
61
+ f = e.ord; return false if f > 47 && f < 58
64
62
  end
65
- return hostnameok
63
+ return true
64
+ end
65
+
66
+ # returns true if the domain part is [IPv4:...] or [IPv6:...].
67
+ # @param [String] email Email address.
68
+ # @return [Boolean] false: the domain part is not a domain literal.
69
+ # true: the domain part is a domain literal.
70
+ def is_domainliteral(email)
71
+ return false if email.is_a?(::String) == false
72
+
73
+ email = email.delete_prefix('<').delete_suffix('>')
74
+ return false if email.size < 16 # e@[IPv4:0.0.0.0] is 16 characters
75
+ return false if email[-1, 1] != ']'
76
+
77
+ lastb = email.rindex('@[IPv'); return false if lastb == false
78
+ dpart = email.split('@')[-1]
79
+
80
+ if email.include?('@[IPv4:')
81
+ # neko@[IPv4:192.0.2.25]
82
+ ipv4a = email[lastb + 7, 16].delete_suffix(']')
83
+ return Sisimai::RFC791.is_ipv4address(ipv4a)
84
+
85
+ elsif email.include?('@[IPv6:')
86
+ # neko@[IPv6:2001:0DB8:0000:0000:0000:0000:0000:0001]
87
+ # neko@[IPv6:2001:0DB8:0000:0000:0000:0000:0000:0001]
88
+ # IPv6-address-literal = "IPv6:" IPv6-addr
89
+ # IPv6-addr = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
90
+ # IPv6-hex = 1*4HEXDIG
91
+ # IPv6-full = IPv6-hex 7(":" IPv6-hex)
92
+ # IPv6-comp = [IPv6-hex *5(":" IPv6-hex)] "::"
93
+ # [IPv6-hex *5(":" IPv6-hex)]
94
+ # ; The "::" represents at least 2 16-bit groups of
95
+ # ; zeros. No more than 6 groups in addition to the
96
+ # ; "::" may be present.
97
+ # IPv6v4-full = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
98
+ # IPv6v4-comp = [IPv6-hex *3(":" IPv6-hex)] "::"
99
+ # [IPv6-hex *3(":" IPv6-hex) ":"]
100
+ # IPv4-address-literal
101
+ # ; The "::" represents at least 2 16-bit groups of
102
+ # ; zeros. No more than 4 groups in addition to the
103
+ # ; "::" and IPv4-address-literal may be present.
104
+ return true if dpart.size > 2 && dpart.rindex(':') > 7
105
+ end
106
+ return false
66
107
  end
67
108
 
68
109
  # find() returns a valid internet hostname found from the argument
@@ -70,8 +111,7 @@ module Sisimai
70
111
  # @return string A valid internet hostname found in the argument
71
112
  # @since v5.2.0
72
113
  def find(argv1 = "")
73
- return "" unless argv1
74
- return "" unless argv1.size > 4
114
+ return "" if argv1.nil? || argv1.size < 5
75
115
 
76
116
  sourcetext = argv1.downcase
77
117
  sourcelist = []
@@ -91,7 +131,7 @@ module Sisimai
91
131
  Sandwiched.each do |e|
92
132
  # Check a hostname exists between the $e->[0] and $e->[1] at array "Sandwiched"
93
133
  # Each array in Sandwiched have 2 elements
94
- next unless Sisimai::String.aligned(sourcetext, e)
134
+ next if Sisimai::String.aligned(sourcetext, e) == false
95
135
 
96
136
  p1 = sourcetext.index(e[0])
97
137
  p2 = sourcetext.index(e[1])
@@ -112,7 +152,7 @@ module Sisimai
112
152
  end
113
153
  ExistUntil.each do |e|
114
154
  # ExistUntil have some strings, not an array
115
- p1 = sourcetext.index(e); next unless p1
155
+ p1 = sourcetext.index(e); next if p1.nil?
116
156
  sourcelist = sourcetext[0, p1].split(" ")
117
157
  throw :MAKELIST
118
158
  end
@@ -126,9 +166,9 @@ module Sisimai
126
166
  e.chop! if e[-1, 1] == "." # Remove "." at the end of the string
127
167
  e.delete!('[]()<>:;')
128
168
 
129
- next unless e.size > 3
130
- next unless e.include?(".")
131
- next unless Sisimai::RFC1123.is_internethost(e)
169
+ next if e.size < 4
170
+ next if e.include?(".") == false
171
+ next if Sisimai::RFC1123.is_internethost(e) == false
132
172
  foundtoken << e
133
173
  end
134
174
  return "" if foundtoken.size == 0
@@ -59,7 +59,7 @@ module Sisimai
59
59
 
60
60
  SubtypeSet = {"addr" => "RFC822", "cdoe" => "SMTP", "host" => "DNS"}.freeze
61
61
  ActionList = ["failed", "delayed", "delivered", "relayed", "expanded"].freeze
62
- Correction = {'deliverable' => 'delivered', 'expired' => 'delayed', 'failure' => 'failed'}
62
+ Correction = {'deliverable' => 'delivered', 'expired' => 'failed', 'failure' => 'failed'}
63
63
  FieldGroup = {
64
64
  'original-recipient' => 'addr',
65
65
  'final-recipient' => 'addr',
@@ -114,16 +114,16 @@ module Sisimai
114
114
 
115
115
  FieldNames[0].each_key do |e|
116
116
  # Per-Message fields
117
- next unless label == e
118
- next unless argv0.include?(FieldNames[0][label])
117
+ next if label != e
118
+ next if argv0.include?(FieldNames[0][label]) == false
119
119
  match = 1; break
120
120
  end
121
121
  return match if match > 0
122
122
 
123
123
  FieldNames[1].each_key do |e|
124
124
  # Per-Recipient field
125
- next unless label == e
126
- next unless argv0.include?(FieldNames[1][label])
125
+ next if label != e
126
+ next if argv0.include?(FieldNames[1][label]) == false
127
127
  match = 2; break
128
128
  end
129
129
  return match
@@ -147,9 +147,7 @@ module Sisimai
147
147
  label = Sisimai::RFC1894.label(argv0)
148
148
  group = FieldGroup[label] || ''
149
149
  parts = argv0.split(":", 2); parts[1] = parts[1].nil? ? "" : Sisimai::String.sweep(parts[1])
150
-
151
- return nil if group.empty?
152
- return nil unless CapturesOn[group]
150
+ return nil if group.empty? || CapturesOn[group].nil?
153
151
 
154
152
  # Try to match with each pattern of Per-Message field, Per-Recipient field
155
153
  # - 0: Field-Name
@@ -9,7 +9,7 @@ module Sisimai
9
9
  # @param [String] argvs String to be checked
10
10
  # @return [Boolean] false: Not MIME encoded string, true: MIME encoded string
11
11
  def is_encoded(argv1)
12
- return false unless argv1
12
+ return false if argv1.nil? || argv1.empty?
13
13
 
14
14
  text1 = argv1.delete('"')
15
15
  mime1 = false
@@ -69,7 +69,7 @@ module Sisimai
69
69
  # utf8 => UTF-8
70
70
  ctxcharset = 'UTF-8' if ctxcharset.casecmp('UTF8') == 0
71
71
 
72
- unless ctxcharset.casecmp('UTF-8') == 0
72
+ if ctxcharset.casecmp('UTF-8') != 0
73
73
  # Characterset is not UTF-8
74
74
  begin
75
75
  p = p.encode!('UTF-8', ctxcharset)
@@ -109,12 +109,11 @@ module Sisimai
109
109
  def parameter(argv0 = '', argv1 = '')
110
110
  return "" if argv0.empty?
111
111
  parameterq = argv1.size > 0 ? argv1 + '=' : ''
112
- paramindex = argv1.size > 0 ? argv0.index(parameterq) : 0
113
- return '' unless paramindex
112
+ paramindex = argv1.size > 0 ? argv0.index(parameterq) : 0; return '' if paramindex.nil?
114
113
 
115
114
  # Find the value of the parameter name specified in argv1
116
115
  foundtoken = argv0[paramindex + parameterq.size, argv0.size].split(';', 2)[0] || ''
117
- foundtoken = foundtoken.downcase unless argv1 == 'boundary'
116
+ foundtoken = foundtoken.downcase if argv1 != 'boundary'
118
117
  foundtoken = foundtoken.delete('"').delete("'")
119
118
  return foundtoken
120
119
  end
@@ -147,8 +146,8 @@ module Sisimai
147
146
  return nil if block.empty?
148
147
 
149
148
  (upperchunk, lowerchunk) = block.split("\n\n", 2)
150
- return ['', ''] if upperchunk.to_s.empty?
151
- return ['', ''] unless upperchunk.index('Content-Type')
149
+ return ['', ''] if upperchunk.nil? || upperchunk.empty?
150
+ return ['', ''] if upperchunk.index('Content-Type').nil?
152
151
 
153
152
  headerpart = ['', ''] # ["text/plain; charset=iso-2022-jp; ...", "quoted-printable"]
154
153
  multipart1 = [] # [headerpart, "body"]
@@ -224,8 +223,10 @@ module Sisimai
224
223
  multiparts = argv1.split(Regexp.new(Regexp.escape(boundary01) + "\n"))
225
224
  partstable = []
226
225
 
227
- multiparts.shift if multiparts[0].size < 8
228
- multiparts.pop if multiparts[-1].size < 8
226
+ # Remove empty or useless preamble and epilogue of multipart/* block
227
+ multiparts.shift if multiparts[0].size < 8
228
+ return [] if multiparts.empty?
229
+ multiparts.pop if multiparts.size > 2 && multiparts[-1].size < 8
229
230
 
230
231
  while e = multiparts.shift do
231
232
  # Check each part and breaks up internal multipart/* block
@@ -234,8 +235,7 @@ module Sisimai
234
235
  # There is nested multipart/* block
235
236
  boundary02 = boundary(f[0], -1); next if boundary02.empty?
236
237
  bodyinside = f[-1].split("\n\n", 2)[-1]
237
- next unless bodyinside.size > 8
238
- next unless bodyinside.index(boundary02)
238
+ next if bodyinside.size < 9 || bodyinside.index(boundary02).nil?
239
239
 
240
240
  v = levelout(f[0], bodyinside)
241
241
  partstable += v if v.size > 0
@@ -299,6 +299,16 @@ module Sisimai
299
299
  if ctencoding == 'base64'
300
300
  # Content-Transfer-Encoding: base64
301
301
  bodystring = decodeB(bodyinside) || ''
302
+ dontappend = false; while first10 = bodystring[0,10] do
303
+ # Don't pick the decoded part as an error message when the part is
304
+ # - BASE64 encoded.
305
+ # - the value of the charset is not utf-8.
306
+ # - NOT a plain text.
307
+ break if Sisimai::String.aligned(e[0], ['charset', '=', 'utf-8'])
308
+ break unless first10 =~ /[\x00-\x08\x0E-\x1F\x7F-]/
309
+ dontappend = true; break
310
+ end
311
+ next if dontappend
302
312
 
303
313
  elsif ctencoding == 'quoted-printable'
304
314
  # Content-Transfer-Encoding: quoted-printable
@@ -328,7 +338,7 @@ module Sisimai
328
338
  # the following errors:
329
339
  # - incompatible character encodings: ASCII-8BIT and UTF-8
330
340
  # - invalid byte sequence in UTF-8
331
- unless bodystring.encoding.to_s == 'UTF-8'
341
+ if bodystring.encoding.to_s != 'UTF-8'
332
342
  # ASCII-8BIT or other 8bit encodings
333
343
  ctxcharset = parameter(e[0], 'charset')
334
344
  if ctxcharset.empty?
@@ -346,6 +356,8 @@ module Sisimai
346
356
 
347
357
  else
348
358
  # There is no Content-Transfer-Encoding header in the part
359
+ be = bodyinside.encoding.to_s
360
+ bodyinside = Sisimai::String.to_utf8(bodyinside, be) if be != 'UTF-8'
349
361
  bodystring += bodyinside
350
362
  end
351
363
 
@@ -357,7 +369,7 @@ module Sisimai
357
369
  end
358
370
 
359
371
  # Append "\n" when the last character of $bodystring is not LF
360
- bodystring += "\n\n" unless bodystring[-2, 2] == "\n\n"
372
+ bodystring += "\n\n" if bodystring[-2, 2] != "\n\n"
361
373
  flattenout += bodystring
362
374
  end
363
375
 
@@ -22,8 +22,7 @@ module Sisimai
22
22
  # @param string argv1 A line of a bounce mail
23
23
  # @return string An MTA name of the 3rd party
24
24
  def returnedby(argv1 = "")
25
- return "" unless argv1
26
- return "" unless argv1.start_with?("X-")
25
+ return "" if argv1.nil? || argv1.start_with?("X-") == false
27
26
 
28
27
  ThirdParty.each_key do |e|
29
28
  # Does the argument include the 3rd party specific field?
@@ -53,7 +52,7 @@ module Sisimai
53
52
  MessagesOf = {
54
53
  "bad-domain" => "hostunknown",
55
54
  "bad-mailbox" => "userunknown",
56
- "inactive-mailbox" => "disabled",
55
+ "inactive-mailbox" => "suspend",
57
56
  "message-expired" => "expired",
58
57
  "no-answer-from-host" => "networkerror",
59
58
  "policy-related" => "policyviolation",
@@ -151,7 +151,7 @@ module Sisimai
151
151
  if o[0] == "final-recipient"
152
152
  # Final-Recipient: rfc822; kijitora@example.jp
153
153
  # Final-Recipient: x400; /PN=...
154
- cv = Sisimai::Address.s3s4(o[2]); next unless Sisimai::Address.is_emailaddress(cv)
154
+ cv = Sisimai::Address.s3s4(o[2]); next if Sisimai::Address.is_emailaddress(cv) == false
155
155
  cw = dscontents.size; next if cw > 0 && cv == dscontents[cw - 1]["recipient"]
156
156
 
157
157
  if v["recipient"] != ""
@@ -177,18 +177,18 @@ module Sisimai
177
177
  # Status: 4.0.0 (cat.example.net: host name lookup failure)
178
178
  v["diagnosis"] += " #{o[4]} "
179
179
  end
180
- next unless FieldTable[o[0]]
180
+ next if FieldTable[o[0]].nil?
181
181
  next if o[3] == "host" && Sisimai::RFC1123.is_internethost(o[2]) == false
182
182
  v[FieldTable[o[0]]] = o[2]
183
183
 
184
- next unless f == 1
184
+ next if f != 1
185
185
  permessage[FieldTable[o[0]]] = o[2]
186
186
  end
187
187
  else
188
188
  # Check that the line is a continued line of the value of Diagnostic-Code: field or not
189
189
  if e.start_with?("X-") && e.include?(": ")
190
190
  # This line is a MTA-Specific fields begins with "X-"
191
- next unless Sisimai::RFC3464::ThirdParty.is3rdparty(e)
191
+ next if Sisimai::RFC3464::ThirdParty.is3rdparty(e) == false
192
192
  cv = Sisimai::RFC3464::ThirdParty.xfield(e)
193
193
 
194
194
  if cv.size > 0 && FieldTable[cv[0].downcase] == nil
@@ -197,7 +197,7 @@ module Sisimai
197
197
  v["reason"] = cv[4][p1 + 1, cv[4].size] if cv[4].start_with?("reason:")
198
198
  else
199
199
  # Set the value picked from "X-*" field to $dscontents when the current value is empty
200
- z = FieldTable[cv[0].downcase]; next unless z
200
+ z = FieldTable[cv[0].downcase]; next if z.nil?
201
201
  v[z] ||= cv[2]
202
202
  end
203
203
  else
@@ -211,7 +211,7 @@ module Sisimai
211
211
 
212
212
  # Diagnostic-Code: SMTP; 550-5.7.26 The MAIL FROM domain [email.example.jp]
213
213
  # has an SPF record with a hard fail
214
- next unless e.start_with?(" ")
214
+ next if e.start_with?(" ") == false
215
215
  v["diagnosis"] += " #{Sisimai::String.sweep(e)}"
216
216
  end
217
217
  end