sisimai 5.0.1 → 5.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/codecovio.yml +33 -0
  3. data/.github/workflows/rake-test.yml +54 -0
  4. data/ChangeLog.md +55 -0
  5. data/Gemfile +2 -0
  6. data/README-JA.md +14 -14
  7. data/README.md +15 -16
  8. data/lib/sisimai/arf.rb +24 -5
  9. data/lib/sisimai/fact.rb +46 -8
  10. data/lib/sisimai/lhost/amazonses.rb +0 -1
  11. data/lib/sisimai/lhost/amazonworkmail.rb +0 -1
  12. data/lib/sisimai/lhost/aol.rb +0 -1
  13. data/lib/sisimai/lhost/bigfoot.rb +0 -1
  14. data/lib/sisimai/lhost/domino.rb +0 -1
  15. data/lib/sisimai/lhost/exchange2007.rb +1 -1
  16. data/lib/sisimai/lhost/exim.rb +7 -16
  17. data/lib/sisimai/lhost/facebook.rb +0 -1
  18. data/lib/sisimai/lhost/googlegroups.rb +2 -1
  19. data/lib/sisimai/lhost/gsuite.rb +0 -1
  20. data/lib/sisimai/lhost/mailmarshalsmtp.rb +1 -1
  21. data/lib/sisimai/lhost/mailru.rb +8 -17
  22. data/lib/sisimai/lhost/messagelabs.rb +0 -1
  23. data/lib/sisimai/lhost/mfilter.rb +1 -1
  24. data/lib/sisimai/lhost/mxlogic.rb +8 -18
  25. data/lib/sisimai/lhost/office365.rb +1 -1
  26. data/lib/sisimai/lhost/outlook.rb +0 -1
  27. data/lib/sisimai/lhost/postfix.rb +0 -1
  28. data/lib/sisimai/lhost/receivingses.rb +0 -1
  29. data/lib/sisimai/lhost/sendgrid.rb +1 -3
  30. data/lib/sisimai/lhost/sendmail.rb +0 -1
  31. data/lib/sisimai/lhost/yandex.rb +0 -1
  32. data/lib/sisimai/message.rb +14 -5
  33. data/lib/sisimai/reason/authfailure.rb +1 -0
  34. data/lib/sisimai/reason/blocked.rb +3 -0
  35. data/lib/sisimai/reason/expired.rb +8 -0
  36. data/lib/sisimai/reason/filtered.rb +1 -0
  37. data/lib/sisimai/reason/mailboxfull.rb +4 -0
  38. data/lib/sisimai/reason/norelaying.rb +1 -0
  39. data/lib/sisimai/reason/rejected.rb +1 -1
  40. data/lib/sisimai/reason/securityerror.rb +1 -0
  41. data/lib/sisimai/reason/spamdetected.rb +1 -0
  42. data/lib/sisimai/reason/suspend.rb +5 -0
  43. data/lib/sisimai/reason/userunknown.rb +4 -1
  44. data/lib/sisimai/rfc5322.rb +120 -64
  45. data/lib/sisimai/rhost/google.rb +82 -67
  46. data/lib/sisimai/rhost/microsoft.rb +8 -0
  47. data/lib/sisimai/rhost/mimecast.rb +9 -2
  48. data/lib/sisimai/rhost/nttdocomo.rb +3 -3
  49. data/lib/sisimai/version.rb +1 -1
  50. data/set-of-emails/maildir/bsd/arf-26.eml +27 -0
  51. data/set-of-emails/maildir/bsd/lhost-sendmail-60.eml +85 -0
  52. data/set-of-emails/maildir/bsd/rhost-microsoft-04.eml +86 -0
  53. data/set-of-emails/maildir/bsd/rhost-microsoft-05.eml +83 -0
  54. metadata +8 -3
  55. data/.travis.yml +0 -23
@@ -69,7 +69,6 @@ module Sisimai::Lhost
69
69
  bodyslices = emailparts[0].split("\n")
70
70
  readcursor = 0 # (Integer) Points the current cursor position
71
71
  recipients = 0 # (Integer) The number of 'Final-Recipient' header
72
- localhost0 = '' # (String) Local MTA
73
72
  v = nil
74
73
 
75
74
  while e = bodyslices.shift do
@@ -145,18 +144,13 @@ module Sisimai::Lhost
145
144
  end
146
145
  return nil unless recipients > 0
147
146
 
148
- unless mhead['received'].empty?
149
- # Get the name of local MTA
150
- # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128])
151
- p1 = mhead['received'][-1].index('from ') || -1
152
- p2 = mhead['received'][-1].index(' ', p1 + 5) || -1
153
- localhost0 = mhead['received'][-1][p1 + 5, p2 - p1 - 5] if p1 > -1
154
- end
147
+ # Get the name of the local MTA
148
+ # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128])
149
+ receivedby = mhead['received'] || []
150
+ recvdtoken = Sisimai::RFC5322.received(receivedby[-1])
155
151
 
156
152
  dscontents.each do |e|
157
- # Set default values if each value is empty.
158
- e['lhost'] ||= localhost0
159
-
153
+ # Check the error message, the rhost, the lhost, and the smtp command.
160
154
  unless e['alterrors'].to_s.empty?
161
155
  # Copy alternative error message
162
156
  e['diagnosis'] ||= e['alterrors']
@@ -175,13 +169,10 @@ module Sisimai::Lhost
175
169
  # host neko.example.jp [192.0.2.222]: 550 5.1.1 <kijitora@example.jp>... User Unknown
176
170
  p1 = e['diagnosis'].index('host ') || -1
177
171
  p2 = e['diagnosis'].index(' ', p1 + 5) || -1
178
- e['rhost'] = e['diagnosis'][p1 + 5, p2 - p1 - 5] if p1 > -1
179
-
180
- unless e['rhost']
181
- # Get localhost and remote host name from Received header.
182
- e['rhost'] = Sisimai::RFC5322.received(mhead['received'][-1]).pop unless mhead['received'].empty?
183
- end
172
+ e['rhost'] = e['diagnosis'][p1 + 5, p2 - p1 - 5] if p1 > -1
173
+ e['rhost'] ||= recvdtoken[1]
184
174
  end
175
+ e['lhost'] ||= recvdtoken[0]
185
176
 
186
177
  unless e['command']
187
178
  # Get the SMTP command name for the session
@@ -98,7 +98,6 @@ module Sisimai::Lhost
98
98
 
99
99
  dscontents.each do |e|
100
100
  # Set default values if each value is empty.
101
- e['lhost'] ||= permessage['rhost']
102
101
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
103
102
  e['command'] = commandset.shift || ''
104
103
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
@@ -104,7 +104,7 @@ module Sisimai::Lhost
104
104
  rhosts = Sisimai::RFC5322.received(rheads[-1])
105
105
 
106
106
  e['lhost'] ||= Sisimai::RFC5322.received(rheads[0]).shift
107
- while ee = rhosts.shift do
107
+ [rhosts[0], rhosts[1]].each do |ee|
108
108
  # Avoid "... by m-FILTER"
109
109
  next unless ee.include?('.')
110
110
  e['rhost'] = ee
@@ -89,7 +89,6 @@ module Sisimai::Lhost
89
89
  bodyslices = emailparts[0].split("\n")
90
90
  readcursor = 0 # (Integer) Points the current cursor position
91
91
  recipients = 0 # (Integer) The number of 'Final-Recipient' header
92
- localhost0 = '' # (String) Local MTA
93
92
  v = nil
94
93
 
95
94
  while e = bodyslices.shift do
@@ -135,19 +134,14 @@ module Sisimai::Lhost
135
134
  end
136
135
  return nil unless recipients > 0
137
136
 
138
- unless mhead['received'].empty?
139
- # Get the name of local MTA
140
- p1 = mhead['received'][-1].downcase.index('from ')
141
- p2 = mhead['received'][-1].index(' ', p1 + 5)
142
-
143
- if (p1 + 1) * (p2 + 1) > 0
144
- # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128])
145
- localhost0 = mhead['received'][-1][p1 + 5, p2 - p1 - 5]
146
- end
147
- end
137
+ # Get the name of the local MTA
138
+ # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128])
139
+ receivedby = mhead['received'] || []
140
+ recvdtoken = Sisimai::RFC5322.received(receivedby[-1])
148
141
 
149
142
  dscontents.each do |e|
150
- e['lhost'] = localhost0
143
+ # Check the error message, the rhost, the lhost, and the smtp command.
144
+ e['lhost'] = recvdtoken[0]
151
145
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'].gsub(/[-]{2}.*\z/, ''))
152
146
 
153
147
  unless e['rhost']
@@ -156,12 +150,8 @@ module Sisimai::Lhost
156
150
  p2 = e['diagnosis'].index(' ', p1 + 5)
157
151
 
158
152
  # host neko.example.jp [192.0.2.222]: 550 5.1.1 <kijitora@example.jp>... User Unknown
159
- e['rhost'] = e['diagnosis'][p1 + 5, p2 - p1 - 5] if p1 > -1
160
-
161
- unless e['rhost']
162
- # Get localhost and remote host name from Received header.
163
- e['rhost'] = Sisimai::RFC5322.received(mhead['received'][-1]).pop unless mhead['received'].empty?
164
- end
153
+ e['rhost'] = e['diagnosis'][p1 + 5, p2 - p1 - 5] if p1 > -1
154
+ e['rhost'] ||= recvdtoken[1]
165
155
  end
166
156
 
167
157
  unless e['command']
@@ -70,7 +70,7 @@ module Sisimai::Lhost
70
70
  %r/\A5[.]7[.]1[23]\z/ => 'rejected',
71
71
  %r/\A5[.]7[.]124\z/ => 'rejected',
72
72
  %r/\A5[.]7[.]13[3-6]\z/ => 'rejected',
73
- %r/\A5[.]7[.]23\z/ => 'blocked',
73
+ %r/\A5[.]7[.]23\z/ => 'authfailure',
74
74
  %r/\A5[.]7[.]25\z/ => 'networkerror',
75
75
  %r/\A5[.]7[.]50[1-3]\z/ => 'spamdetected',
76
76
  %r/\A5[.]7[.]50[4-5]\z/ => 'filtered',
@@ -97,7 +97,6 @@ module Sisimai::Lhost
97
97
 
98
98
  dscontents.each do |e|
99
99
  # Set default values if each value is empty.
100
- e['lhost'] ||= permessage['rhost']
101
100
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
102
101
 
103
102
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis']) || ''
@@ -227,7 +227,6 @@ module Sisimai::Lhost
227
227
 
228
228
  dscontents.each do |e|
229
229
  # Set default values if each value is empty.
230
- e['lhost'] ||= permessage['rhost']
231
230
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
232
231
 
233
232
  if anotherset['diagnosis']
@@ -96,7 +96,6 @@ module Sisimai::Lhost
96
96
 
97
97
  dscontents.each do |e|
98
98
  # Set default values if each value is empty.
99
- e['lhost'] ||= permessage['rhost']
100
99
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
101
100
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'].tr("\n", ' '))
102
101
 
@@ -124,6 +124,7 @@ module Sisimai::Lhost
124
124
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
125
125
  e['replycode'] = Sisimai::SMTP::Reply.find(e['diagnosis']) || ''
126
126
  e['status'] = e['replycode'][0, 1] + '.0.0' if e['replycode'].size == 3
127
+ e['command'] = thecommand
127
128
 
128
129
  if e['status'] == '5.0.0' || e['status'] == '4.0.0'
129
130
  # Get the value of D.S.N. from the error message or the value of Diagnostic-Code header.
@@ -138,9 +139,6 @@ module Sisimai::Lhost
138
139
  e['status'] = Sisimai::SMTP::Status.code('expired') || e['status']
139
140
  end
140
141
  end
141
-
142
- e['lhost'] ||= permessage['rhost']
143
- e['command'] = thecommand
144
142
  end
145
143
 
146
144
  return { 'ds' => dscontents, 'rfc822' => emailparts[1] }
@@ -168,7 +168,6 @@ module Sisimai::Lhost
168
168
  dscontents.each do |e|
169
169
  # Set default values if each value is empty.
170
170
  e['diagnosis'] ||= ''
171
- e['lhost'] ||= permessage['rhost']
172
171
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
173
172
 
174
173
  if anotherset['diagnosis']
@@ -103,7 +103,6 @@ module Sisimai::Lhost
103
103
 
104
104
  dscontents.each do |e|
105
105
  # Set default values if each value is empty.
106
- e['lhost'] ||= permessage['rhost']
107
106
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
108
107
 
109
108
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'].tr("\n", ' '))
@@ -198,17 +198,26 @@ module Sisimai
198
198
  # Select and convert all the headers in $argv0. The following regular expression is based on
199
199
  # https://gist.github.com/xtetsuji/b080e1f5551d17242f6415aba8a00239
200
200
  headermaps = { 'subject' => '' }
201
- recvheader = []
201
+ receivedby = []
202
202
  argv0.scan(/^([\w-]+):[ ]*(.*?)\n(?![\s\t])/m) { |e| headermaps[e[0].downcase] = e[1] }
203
203
  headermaps.delete('received')
204
204
  headermaps.each_key { |e| headermaps[e].gsub!(/\n[\s\t]+/, ' ') }
205
205
 
206
206
  if argv0.include?('Received:')
207
207
  # Capture values of each Received: header
208
- recvheader = argv0.scan(/^Received:[ ]*(.*?)\n(?![\s\t])/m).flatten
209
- recvheader.each { |e| e.gsub!(/\n[\s\t]+/, ' ') }
208
+ re = argv0.scan(/^Received:[ ]*(.*?)\n(?![\s\t])/m).flatten
209
+ re.each do |e|
210
+ # 1. Exclude the Received header including "(qmail ** invoked from network)".
211
+ # 2. Convert all consecutive spaces and line breaks into a single space character.
212
+ next if e.include?(' invoked by uid')
213
+ next if e.include?(' invoked from network')
214
+
215
+ e.gsub!(/\n[\s\t]+/, ' ')
216
+ e.squeeze!("\n\t ")
217
+ receivedby << e
218
+ end
210
219
  end
211
- headermaps['received'] = recvheader
220
+ headermaps['received'] = receivedby
212
221
 
213
222
  return headermaps unless argv1
214
223
  return headermaps if headermaps['subject'].empty?
@@ -443,7 +452,7 @@ module Sisimai
443
452
  unless haveloaded['Sisimai::ARF']
444
453
  # Feedback Loop message
445
454
  require 'sisimai/arf'
446
- havesifted = Sisimai::ARF.inquire(mailheader, bodystring) if Sisimai::ARF.is_arf(mailheader)
455
+ havesifted = Sisimai::ARF.inquire(mailheader, bodystring)
447
456
  throw :PARSER if havesifted
448
457
  end
449
458
 
@@ -20,6 +20,7 @@ module Sisimai
20
20
  'dmarc policy',
21
21
  'please inspect your spf settings',
22
22
  'sender policy framework (spf) fail',
23
+ 'sender policy framework violation',
23
24
  'spf (sender policy framework) domain authentication fail',
24
25
  'spf check: fail',
25
26
  ].freeze
@@ -46,6 +46,7 @@ module Sisimai
46
46
  'part of their network is on our block list',
47
47
  'please use the smtp server of your isp',
48
48
  'refused - see http',
49
+ 'rejected - multi-blacklist', # junkemailfilter.com
49
50
  'rejected because the sending mta or the sender has not passed validation',
50
51
  'rejecting open proxy', # Sendmail(srvrsmtp.c)
51
52
  'sender ip address rejected',
@@ -61,6 +62,7 @@ module Sisimai
61
62
  'we do not accept mail from dynamic ips', # @mail.ru
62
63
  'you are not allowed to connect',
63
64
  'you are sending spam',
65
+ 'your ip address is listed in the rbl',
64
66
  'your network is temporary blacklisted',
65
67
  'your server requires confirmation',
66
68
  ].freeze
@@ -78,6 +80,7 @@ module Sisimai
78
80
  ['message from ', ' rejected based on blacklist'],
79
81
  ['messages from ', ' temporarily deferred due to user complaints'], # Yahoo!
80
82
  ['server ip ', ' listed as abusive'],
83
+ ['sorry! your ip address', ' is blocked by rbl'], # junkemailfilter.com
81
84
  ['the domain ', ' is blacklisted'],
82
85
  ['the email ', ' is blacklisted'],
83
86
  ['the ip', ' is blacklisted'],
@@ -7,10 +7,13 @@ module Sisimai
7
7
  # the message you sent has been in the queue for long time.
8
8
  module Expired
9
9
  class << self
10
+ require 'sisimai/string'
11
+
10
12
  Index = [
11
13
  'connection timed out',
12
14
  'could not find a gateway for',
13
15
  'delivery attempts will continue to be',
16
+ 'delivery expired',
14
17
  'delivery time expired',
15
18
  'failed to deliver to domain ',
16
19
  'giving up on',
@@ -18,6 +21,7 @@ module Sisimai
18
21
  'has been delayed',
19
22
  'it has not been collected after',
20
23
  'message expired after sitting in queue for',
24
+ 'message expired, cannot connect to remote server',
21
25
  'message expired, connection refulsed',
22
26
  'message timed out',
23
27
  'retry time not reached for any host after a long failure period',
@@ -27,6 +31,9 @@ module Sisimai
27
31
  'was not reachable within the allowed queue period',
28
32
  'your message could not be delivered for more than',
29
33
  ].freeze
34
+ Pairs = [
35
+ ['could not be delivered for', ' days'],
36
+ ].freeze
30
37
 
31
38
  def text; return 'expired'; end
32
39
  def description; return 'Delivery time has expired due to a connection failure'; end
@@ -38,6 +45,7 @@ module Sisimai
38
45
  def match(argv1)
39
46
  return nil unless argv1
40
47
  return true if Index.any? { |a| argv1.include?(a) }
48
+ return true if Pairs.any? { |a| Sisimai::String.aligned(argv1, a) }
41
49
  return false
42
50
  end
43
51
 
@@ -21,6 +21,7 @@ module Sisimai
21
21
  'resolver.rst.notauthorized', # Microsoft Exchange
22
22
  'this account is protected by',
23
23
  'user not found', # Filter on MAIL.RU
24
+ 'user refuses to receive this mail',
24
25
  'user reject',
25
26
  'we failed to deliver mail because the following address recipient id refuse to receive mail', # Willcom
26
27
  'you have been blocked by the recipient',
@@ -8,6 +8,7 @@ module Sisimai
8
8
  module MailboxFull
9
9
  class << self
10
10
  Index = [
11
+ '452 insufficient disk space',
11
12
  'account disabled temporarly for exceeding receiving limits',
12
13
  'account is exceeding their quota',
13
14
  'account is over quota',
@@ -15,6 +16,7 @@ module Sisimai
15
16
  'boite du destinataire pleine',
16
17
  'delivery failed: over quota',
17
18
  'disc quota exceeded',
19
+ 'diskspace quota',
18
20
  'does not have enough space',
19
21
  'exceeded storage allocation',
20
22
  'exceeding its mailbox quota',
@@ -34,7 +36,9 @@ module Sisimai
34
36
  'maildir delivery failed: userdisk quota ',
35
37
  'maildir delivery failed: domaindisk quota ',
36
38
  'mailfolder is full',
39
+ 'no space left on device',
37
40
  'not enough storage space in',
41
+ 'not sufficient disk space',
38
42
  'over the allowed quota',
39
43
  'quota exceeded',
40
44
  'quota violation for',
@@ -24,6 +24,7 @@ module Sisimai
24
24
  'relay not permitted',
25
25
  'relaying denied', # Sendmail
26
26
  'relaying mail to ',
27
+ 'specified domain is not allowed',
27
28
  "that domain isn't in my list of allowed rcpthost",
28
29
  'this system is not configured to relay mail',
29
30
  'unable to relay for',
@@ -38,7 +38,7 @@ module Sisimai
38
38
  'email address is on senderfilterconfig list',
39
39
  'emetteur invalide',
40
40
  'empty envelope senders not allowed',
41
- 'envelope blocked ',
41
+ 'envelope blocked - ',
42
42
  'error: no third-party dsns', # SpamWall - block empty sender
43
43
  'from: domain is invalid. please provide a valid from:',
44
44
  'fully qualified email address required', # McAfee
@@ -29,6 +29,7 @@ module Sisimai
29
29
  'insecure mail relay',
30
30
  'recipient address rejected: access denied',
31
31
  "sorry, you don't authenticate or the domain isn't in my list of allowed rcpthosts",
32
+ 'starttls is required to send mail',
32
33
  'tls required but not supported', # SendGrid:the recipient mailserver does not support TLS or have a valid certificate
33
34
  'unauthenticated senders not allowed',
34
35
  'verification failure',
@@ -76,6 +76,7 @@ module Sisimai
76
76
  'spam score ',
77
77
  'spambouncer identified spam', # SpamBouncer identified SPAM
78
78
  'spamming not allowed',
79
+ 'too many spam complaints',
79
80
  'too much spam.', # Earthlink
80
81
  'the email message was detected as spam',
81
82
  'the message has been rejected by spam filtering engine',
@@ -12,16 +12,21 @@ module Sisimai
12
12
  'boite du destinataire archivee',
13
13
  'email account that you tried to reach is disabled',
14
14
  'has been suspended',
15
+ 'inactive account',
15
16
  'invalid/inactive user',
16
17
  'is a deactivated mailbox', # http://service.mail.qq.com/cgi-bin/help?subtype=1&&id=20022&&no=1000742
17
18
  'is unavailable: user is terminated',
18
19
  'mailbox currently suspended',
20
+ 'mailbox disabled',
19
21
  'mailbox is frozen',
20
22
  'mailbox unavailable or access denied',
21
23
  'recipient rejected: temporarily inactive',
22
24
  'recipient suspend the service',
23
25
  'this account has been disabled or discontinued',
26
+ 'this account has been temporarily suspended',
27
+ 'this address no longer accepts mail',
24
28
  'this mailbox is disabled',
29
+ 'user or domain is disabled',
25
30
  'user suspended', # http://mail.163.com/help/help_spam_16.htm
26
31
  'vdelivermail: account is locked email bounced',
27
32
  ].freeze
@@ -63,7 +63,6 @@ module Sisimai
63
63
  'no such mailbox',
64
64
  'no such person at this address',
65
65
  'no such recipient',
66
-
67
66
  'no such user',
68
67
  'no thank you rejected: account unavailable',
69
68
  'no valid recipients, bye',
@@ -76,7 +75,9 @@ module Sisimai
76
75
  'recipient address rejected: invalid user',
77
76
  'recipient address rejected: invalid-recipient',
78
77
  'recipient address rejected: unknown user',
78
+ 'recipient address rejected: userunknown',
79
79
  'recipient does not exist',
80
+ 'recipient is not accepted',
80
81
  'recipient is not local',
81
82
  'recipient not exist',
82
83
  'recipient not found',
@@ -125,6 +126,7 @@ module Sisimai
125
126
  ['no ', ' in name directory'],
126
127
  ['non', 'existent user'],
127
128
  ['rcpt <', ' does not exist'],
129
+ ['rcpt (', 't exist '],
128
130
  ['recipient ', ' was not found in'],
129
131
  ['recipient address rejected: user ', ' does not exist'],
130
132
  ['recipient address rejected: user unknown in ', ' table'],
@@ -135,6 +137,7 @@ module Sisimai
135
137
  ['unknown local', 'part'],
136
138
  ['user ', ' was not found'],
137
139
  ['user ', ' does not exist'],
140
+ ['user (', ') unknown'],
138
141
  ].freeze
139
142
 
140
143
  def text; return 'userunknown'; end
@@ -3,6 +3,7 @@ module Sisimai
3
3
  module RFC5322
4
4
  class << self
5
5
  require 'sisimai/string'
6
+ require 'sisimai/address'
6
7
  HeaderTable = {
7
8
  :messageid => %w[message-id],
8
9
  :subject => %w[subject],
@@ -52,79 +53,134 @@ module Sisimai
52
53
  # @return [Array] Received header as a structured data
53
54
  def received(argv1)
54
55
  return [] unless argv1.is_a?(::String)
56
+ return [] if argv1.include?(' invoked by uid')
57
+ return [] if argv1.include?(' invoked from network')
58
+
59
+ # - https://datatracker.ietf.org/doc/html/rfc5322
60
+ # received = "Received:" *received-token ";" date-time CRLF
61
+ # received-token = word / angle-addr / addr-spec / domain
62
+ #
63
+ # - Appendix A.4. Message with Trace Fields
64
+ # Received:
65
+ # from x.y.test
66
+ # by example.net
67
+ # via TCP
68
+ # with ESMTP
69
+ # id ABC12345
70
+ # for <mary@example.net>; 21 Nov 1997 10:05:43 -0600
71
+ recvd = argv1.split(' ')
72
+ label = %w[from by via with id for]
73
+ token = {}
74
+ other = []
75
+ alter = []
76
+ right = false
77
+ range = recvd.size
78
+ index = -1
79
+
80
+ recvd.each do |e|
81
+ # Look up each label defined in "label" from Received header
82
+ index += 1
83
+ break unless index < range; f = e.downcase
84
+ next unless label.any? { |a| f == a }
85
+ token[f] = recvd[index + 1] || next
86
+ token[f] = token[f].downcase.delete('();')
87
+
88
+ next unless f == 'from'
89
+ break unless index + 2 < range
90
+ next unless recvd[index + 2].start_with?('(')
91
+
92
+ # Get and keep a hostname in the comment as follows:
93
+ # from mx1.example.com (c213502.kyoto.example.ne.jp [192.0.2.135]) by mx.example.jp (V8/cf)
94
+ # [
95
+ # "from", # index + 0
96
+ # "mx1.example.com", # index + 1
97
+ # "(c213502.kyoto.example.ne.jp", # index + 2
98
+ # "[192.0.2.135])", # index + 3
99
+ # "by",
100
+ # "mx.example.jp",
101
+ # "(V8/cf)",
102
+ # ...
103
+ # ]
104
+ # The 2nd element after the current element is NOT a continuation of the current element
105
+ # such as "(c213502.kyoto.example.ne.jp)"
106
+ other << recvd[index + 2].delete('();')
107
+
108
+ # The 2nd element after the current element is a continuation of the current element.
109
+ # such as "(c213502.kyoto.example.ne.jp", "[192.0.2.135])"
110
+ break unless index + 3 < range
111
+ other << recvd[index + 3].delete('();')
112
+ end
55
113
 
56
- hosts = []
57
- value = { 'from' => '', 'by' => '' }
58
-
59
- # Received: (qmail 10000 invoked by uid 999); 24 Apr 2013 00:00:00 +0900
60
- return [] if argv1.include?('(qmail ') && argv1.include?(' invoked ')
61
-
62
- p1 = argv1.index('from ') || -1
63
- p2 = argv1.index('by ') || -1
64
- p3 = argv1.index(' ', p2 + 3) || -1
65
-
66
- if p1 == 0 && p2 > 1 && p2 < p3
67
- # Received: from localhost (localhost) by nijo.example.jp (V8/cf) id s1QB5ma0018057;
68
- # Wed, 26 Feb 2014 06:05:48 -0500
69
- value['from'] = Sisimai::String.sweep(argv1[p1 + 5, p2 - p1 - 5])
70
- value['by'] = Sisimai::String.sweep(argv1[p2 + 3, p3 - p2 - 3])
114
+ other.each do |e|
115
+ # Check alternatives in "other", and then delete uninformative values.
116
+ next if e.nil?
117
+ next if e.size < 4
118
+ next if e == 'unknown'
119
+ next if e == 'localhost'
120
+ next if e == '[127.0.0.1]'
121
+ next if e == '[IPv6:::1]'
122
+ next unless e.include?('.')
123
+ next if e.include?('=')
124
+ alter << e
125
+ end
71
126
 
72
- elsif p1 != 0 && p2 > -1
73
- # Received: by 10.70.22.98 with SMTP id c2mr1838265pdf.3; Fri, 18 Jul 2014 00:31:02 -0700 (PDT)
74
- value['from'] = Sisimai::String.sweep(argv1[p2 + 3, argv1.size])
75
- value['by'] = Sisimai::String.sweep(argv1[p2 + 3, p3 - p2 - 3])
127
+ %w[from by].each do |e|
128
+ # Remove square brackets from the IP address such as "[192.0.2.25]"
129
+ next if token[e].nil?
130
+ next if token[e].empty?
131
+ next unless token[e].start_with?('[')
132
+ token[e] = Sisimai::String.ipv4(token[e]).shift || ''
76
133
  end
134
+ token['from'] ||= ''
77
135
 
78
- if value['from'].include?(' ')
79
- # Received: from [10.22.22.222] (smtp.kyoto.ocn.ne.jp [192.0.2.222]) (authenticated bits=0)
80
- # by nijo.example.jp (V8/cf) with ESMTP id s1QB5ka0018055; Wed, 26 Feb 2014 06:05:47 -0500
81
- received = value['from'].split(' ')
82
- namelist = []
83
- addrlist = []
84
- hostname = ''
85
- hostaddr = ''
86
-
87
- while e = received.shift do
88
- # Received: from [10.22.22.222] (smtp-gateway.kyoto.ocn.ne.jp [192.0.2.222])
89
- cv = Sisimai::String.ipv4(e) || []
90
- if cv.size > 0
91
- # [192.0.2.1] or (192.0.2.1)
92
- addrlist.append(*cv)
93
- else
94
- # hostname
95
- e = e.delete('()').strip
96
- namelist << e
97
- end
98
- end
136
+ while true do
137
+ # Prefer hostnames over IP addresses, except for localhost.localdomain and similar.
138
+ break if token['from'] == 'localhost'
139
+ break if token['from'] == 'localhost.localdomain'
140
+ break unless token['from'].include?('.') # A hostname without a domain name
141
+ break unless Sisimai::String.ipv4(token['from']).empty?
99
142
 
100
- while e = namelist.shift do
101
- # 1. Hostname takes priority over all other IP addresses
102
- next unless e.include?('.')
103
- hostname = e
104
- break
105
- end
143
+ # No need to rewrite token['from']
144
+ right = true
145
+ break
146
+ end
106
147
 
107
- if hostname.empty?
108
- # 2. Use IP address as a remote host name
109
- addrlist.each do |e|
110
- # Skip if the address is a private address
111
- next if e.start_with?('10.', '127.', '192.168.')
112
- next if e =~ /\A172[.](?:1[6-9]|2[0-9]|3[0-1])[.]/
113
- hostaddr = e
114
- break
115
- end
148
+ while true do
149
+ # Try to rewrite uninformative hostnames and IP addresses in token['from']
150
+ break if right # There is no need to rewrite
151
+ break if alter.empty? # There is no alternative to rewriting
152
+ break if alter[0].include?(token['from'])
153
+
154
+ if token['from'].start_with?('localhost')
155
+ # localhost or localhost.localdomain
156
+ token['from'] = alter[0]
157
+ elsif token['from'].index('.')
158
+ # A hostname without a domain name such as "mail", "mx", or "mbox"
159
+ token['from'] = alter[0] if alter[0].include?('.')
160
+ else
161
+ # An IPv4 address
162
+ token['from'] = alter[0]
116
163
  end
117
-
118
- value['from'] = hostname || hostaddr || addrlist[-1]
164
+ break
119
165
  end
120
-
121
- %w[from by].each do |e|
122
- # Copy entries into hosts
123
- next if value[e].empty?
124
- value[e] = value[e].delete('[]();?')
125
- hosts << value[e]
166
+ token.delete('from') if token['from'].nil?
167
+ token.delete('by') if token['by'].nil?
168
+ token['for'] = Sisimai::Address.s3s4(token['for']) if token.has_key?('for')
169
+
170
+ token.keys.each do |e|
171
+ # Delete an invalid value
172
+ token[e] = '' if token[e].include?(' ')
173
+ token[e].delete!('[]') # Remove "[]" from the IP address
126
174
  end
127
- return hosts
175
+
176
+ return [
177
+ token['from'] || '',
178
+ token['by'] || '',
179
+ token['via'] || '',
180
+ token['with'] || '',
181
+ token['id'] || '',
182
+ token['for'] || '',
183
+ ]
128
184
  end
129
185
 
130
186
  # Split given entire message body into error message lines and the original message part only