sisimai 5.0.1 → 5.0.2

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake-test.yml +55 -0
  3. data/ChangeLog.md +27 -0
  4. data/README-JA.md +13 -13
  5. data/README.md +13 -14
  6. data/lib/sisimai/fact.rb +46 -8
  7. data/lib/sisimai/lhost/amazonses.rb +0 -1
  8. data/lib/sisimai/lhost/amazonworkmail.rb +0 -1
  9. data/lib/sisimai/lhost/aol.rb +0 -1
  10. data/lib/sisimai/lhost/bigfoot.rb +0 -1
  11. data/lib/sisimai/lhost/domino.rb +0 -1
  12. data/lib/sisimai/lhost/exchange2007.rb +1 -1
  13. data/lib/sisimai/lhost/exim.rb +7 -16
  14. data/lib/sisimai/lhost/facebook.rb +0 -1
  15. data/lib/sisimai/lhost/googlegroups.rb +2 -1
  16. data/lib/sisimai/lhost/gsuite.rb +0 -1
  17. data/lib/sisimai/lhost/mailru.rb +8 -17
  18. data/lib/sisimai/lhost/messagelabs.rb +0 -1
  19. data/lib/sisimai/lhost/mfilter.rb +1 -1
  20. data/lib/sisimai/lhost/mxlogic.rb +8 -18
  21. data/lib/sisimai/lhost/office365.rb +1 -1
  22. data/lib/sisimai/lhost/outlook.rb +0 -1
  23. data/lib/sisimai/lhost/postfix.rb +0 -1
  24. data/lib/sisimai/lhost/receivingses.rb +0 -1
  25. data/lib/sisimai/lhost/sendgrid.rb +1 -3
  26. data/lib/sisimai/lhost/sendmail.rb +0 -1
  27. data/lib/sisimai/lhost/yandex.rb +0 -1
  28. data/lib/sisimai/message.rb +13 -4
  29. data/lib/sisimai/reason/authfailure.rb +1 -0
  30. data/lib/sisimai/reason/blocked.rb +2 -0
  31. data/lib/sisimai/reason/expired.rb +1 -0
  32. data/lib/sisimai/reason/mailboxfull.rb +1 -0
  33. data/lib/sisimai/reason/securityerror.rb +1 -0
  34. data/lib/sisimai/reason/spamdetected.rb +1 -0
  35. data/lib/sisimai/reason/suspend.rb +1 -0
  36. data/lib/sisimai/rfc5322.rb +120 -64
  37. data/lib/sisimai/rhost/mimecast.rb +9 -2
  38. data/lib/sisimai/version.rb +1 -1
  39. data/set-of-emails/maildir/bsd/lhost-sendmail-60.eml +85 -0
  40. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f3ea96ca0a6350d1b5b085191d287c061459de6be048ecc6cc1a115509adcef
4
- data.tar.gz: 2305366582fd23ba18f9db19d21b33c6d714f871435aa6b2601b3c421a749fe9
3
+ metadata.gz: 7090710f97dbdd17e7bf971fdad787f7eeab1656de2e93012e32430b9a7d64fb
4
+ data.tar.gz: 4dd096852de6cb0c9a44a78fc11058a86494fb4e032f8aa6c8a31bf65149cec8
5
5
  SHA512:
6
- metadata.gz: 795bf696fc7accdcb2835f9cdd719e3c61dc3b84a8dec864e65fc325a1499dde22593edba19584ba47bb6b485c90ce9fbfb49a962b3cd920695c6564eef28a9c
7
- data.tar.gz: 4b28f1877c47f788ba5ad72bce4a22931b651534c5c7096c3dd30c5c75f11fcc4401c605915e2becdf168015a471a04a73cf7b3fca0f85e1cfeb11073057a4b4
6
+ metadata.gz: d4f9752e00950e8965c6328ddea6f82f80e436e9e7788d41ab9e5484fce8a64be99c23d193d902f118d158eed5246b4601f4c9ce618cd219e04a943db79df860
7
+ data.tar.gz: ee66c1d20a46c49a11fc07c111039a3322fc89cd2e682d9b09a59040f35fa3d2a93348c975f7d6cb10884d29c80962df4e6c65e51e7a90dfe03b2fd0c36cad4e
@@ -0,0 +1,55 @@
1
+ name: rake test
2
+ on:
3
+ push:
4
+ branches: ["5-stable"]
5
+ pull_request:
6
+ branches: ["5-stable"]
7
+ jobs:
8
+ test-cruby:
9
+ name: rake test with CRuby ${{ matrix.cruby }}
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ cruby: ["2.4", "3.3"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - name: Setup CRuby
18
+ uses: ruby/setup-ruby@v1
19
+ with:
20
+ ruby-version: ${{ matrix.cruby }}
21
+ - run: gem install minitest -v 5.15.0
22
+ - run: gem install oj -v 3.10.0
23
+ - run: gem install bundle rake
24
+ - name: Cache the dependent gems
25
+ uses: actions/cache@v2
26
+ with:
27
+ path: ./cache
28
+ key: ${{ runner.os }}-ruby-${{ matrix.cruby }}-${{ hashFiles('./gem.snapshot') }}
29
+ - run: ruby -v
30
+ - run: rake publictest
31
+ - run: rake privatetest
32
+ test-jruby:
33
+ name: rake test with JRuby ${{ matrix.jruby }}
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ fail-fast: false
37
+ matrix:
38
+ jruby: ["jruby-9.2", "jruby-9.4"]
39
+ steps:
40
+ - uses: actions/checkout@v2
41
+ - name: Setup JRuby
42
+ uses: ruby/setup-ruby@v1
43
+ with:
44
+ ruby-version: ${{ matrix.jruby }}
45
+ - run: gem install minitest -v 5.15.0
46
+ - run: gem install bundle rake jrjackson
47
+ - name: Cache the dependent gems
48
+ uses: actions/cache@v2
49
+ with:
50
+ path: ./cache
51
+ key: ${{ runner.os }}-ruby-${{ matrix.jruby }}-${{ hashFiles('./gem.snapshot') }}
52
+ - run: ruby -v
53
+ - run: rake publictest
54
+ - run: rake privatetest
55
+
data/ChangeLog.md CHANGED
@@ -3,6 +3,33 @@ RELEASE NOTES for Ruby version of Sisimai
3
3
  - releases: "https://github.com/sisimai/rb-sisimai/releases"
4
4
  - download: "https://rubygems.org/gems/sisimai"
5
5
 
6
+ v5.0.2
7
+ ---------------------------------------------------------------------------------------------------
8
+ - release: "Wed, 13 Mar 2024 13:00:00 +0900 (JST)"
9
+ - version: "5.0.2"
10
+ - changes:
11
+ - #271 #267 Sisimai 5 works on JRuby again. Thanks to @hiroyuki-sato
12
+ - #159 Implement workarounds at `Sisimai::Fact` and some public tests with `DateTime` class to
13
+ run Sisimai on JRuby, to avoid failing `strptime()`
14
+ - #269 Replace `Array#append` with `Array#push` at `Sisimai::RFC5322` because `Array#append` is
15
+ only available in Ruby 2.5 and above
16
+ - We have started testing JRuby 9.2 and 9.4 on GitHub Actions
17
+ - Disable 2 tests in `test/public/mail-test.rb` that fails on GitHub Actions only
18
+ - `5.7.23` returned from Office365 is an error related to SPF vilation (authfailure)
19
+ - #272 Fixed an issue that Sisimai could not get the value of `alias` address correctly when an
20
+ email forwarded and bounced
21
+ - `Sisimai::RFC5322.received` now returns a list including all the elements except date time and
22
+ (comments) found in the `Received` header
23
+ - Update the error message patterns in `Sisimai::Rhost::Mimecast`
24
+ - Update the error message patterns in the followings:
25
+ - `AuthFailure`
26
+ - `Blocked`
27
+ - `Expired`
28
+ - `MailboxFull`
29
+ - `SecurityError`
30
+ - `SpamDetected`
31
+ - `Suspend`
32
+
6
33
  v5.0.1
7
34
  ---------------------------------------------------------------------------------------------------
8
35
  - release: "Sun, 3 Mar 2024 17:17:17 +0900 (JST)"
data/README-JA.md CHANGED
@@ -102,7 +102,7 @@ Sisimaiの動作環境についての詳細は[Sisimai | シシマイを使っ
102
102
 
103
103
  * [Ruby 2.4.0 or later](http://www.ruby-lang.org/)
104
104
  * [__oj | The fastest JSON parser and object serializer__](https://rubygems.org/gems/oj)
105
- * Also works on [JRuby 9.0.4.0 - 9.1.17.0](http://jruby.org)
105
+ * Also works on [JRuby 9.2 or later](http://jruby.org)
106
106
  * [__jrjackson | A mostly native JRuby wrapper for the java jackson json processor jar__](https://rubygems.org/gems/jrjackson)
107
107
 
108
108
  Install
@@ -110,10 +110,10 @@ Install
110
110
  ### From RubyGems.org
111
111
  ```shell
112
112
  $ sudo gem install sisimai
113
- Fetching: sisimai-5.0.1.gem (100%)
114
- Successfully installed sisimai-5.0.1
115
- Parsing documentation for sisimai-5.0.1
116
- Installing ri documentation for sisimai-5.0.1
113
+ Fetching: sisimai-5.0.2.gem (100%)
114
+ Successfully installed sisimai-5.0.2
115
+ Parsing documentation for sisimai-5.0.2
116
+ Installing ri documentation for sisimai-5.0.2
117
117
  Done installing documentation for sisimai after 6 seconds
118
118
  1 gem installed
119
119
  ```
@@ -141,13 +141,13 @@ if [ -d "/usr/local/jr" ]; then \
141
141
  ...
142
142
  3 gems installed
143
143
  /opt/local/bin/rake install
144
- sisimai 5.0.0 built to pkg/sisimai-5.0.0.gem.
145
- sisimai (5.0.0) installed.
144
+ sisimai 5.0.2 built to pkg/sisimai-5.0.2.gem.
145
+ sisimai (5.0.2) installed.
146
146
  if [ -d "/usr/local/jr" ]; then \
147
147
  PATH="/usr/local/jr/bin:$PATH" /usr/local/jr/bin/rake install; \
148
148
  fi
149
- sisimai 5.0.0 built to pkg/sisimai-5.0.0-java.gem.
150
- sisimai (5.0.0) installed.
149
+ sisimai 5.0.2 built to pkg/sisimai-5.0.2-java.gem.
150
+ sisimai (5.0.2) installed.
151
151
  ```
152
152
 
153
153
  Usage
@@ -348,15 +348,15 @@ Sisimai 5.0.0から**Ruby 2.4以上**が必要になります。
348
348
 
349
349
  | 機能 | Sisimai 4 | Sisimai 5 |
350
350
  |------------------------------------------------------|--------------------|---------------------|
351
- | 動作環境(CRuby) | 2.1 - | **2.4** - 3.3.0 |
352
- | 動作環境(JRuby) | 9.0.4.0 - 9.1.17.0 | 9.0.4.0 - 9.1.17.0 |
351
+ | 動作環境(CRuby) | 2.1 - | **2.4** or later |
352
+ | 動作環境(JRuby) | 9.0.4.0 - 9.1.17.0 | **9.2** or later |
353
353
  | 元メールファイルを操作可能なコールバック機能 | なし | あり[^3] |
354
354
  | 解析エンジン(MTA/ESPモジュール)の数 | 68 | 70 |
355
355
  | 検出可能なバウンス理由の数 | 29 | 34 |
356
356
  | 依存Gem数(Ruby Standard Gemsを除く) | 1 Gem | 1 Gem |
357
- | ソースコードの行数 | 10,800 行 | 11,400 行 |
357
+ | ソースコードの行数 | 10,800 行 | 11,370 行 |
358
358
  | テストフレームワーク | rspec | minitest |
359
- | テスト件数(spec/またはtest/ディレクトリ) | 311,000 件 | 336,000 件 |
359
+ | テスト件数(spec/またはtest/ディレクトリ) | 311,000 件 | 338,000 件 |
360
360
  | 1秒間に解析できるバウンスメール数[^4] | 231 通 | 305 通 |
361
361
  | ライセンス | 2条項BSD | 2条項BSD |
362
362
  | 開発会社による商用サポート | 提供中 | 提供中 |
data/README.md CHANGED
@@ -99,10 +99,9 @@ System requirements
99
99
  More details about system requirements are available at
100
100
  [Sisimai | Getting Started](https://libsisimai.org/en/start/) page.
101
101
 
102
-
103
102
  * [Ruby 2.4.0 or later](http://www.ruby-lang.org/)
104
103
  * [__oj | The fastest JSON parser and object serializer__](https://rubygems.org/gems/oj)
105
- * Also works on [JRuby 9.0.4.0 - 9.1.17.0](http://jruby.org)
104
+ * Also works on [JRuby 9.2 or later](http://jruby.org)~~
106
105
  * [__jrjackson | A mostly native JRuby wrapper for the java jackson json processor jar__](https://rubygems.org/gems/jrjackson)
107
106
 
108
107
  Install
@@ -110,10 +109,10 @@ Install
110
109
  ### From RubyGems
111
110
  ```shell
112
111
  $ sudo gem install sisimai
113
- Fetching: sisimai-5.0.1.gem (100%)
114
- Successfully installed sisimai-5.0.1
115
- Parsing documentation for sisimai-5.0.1
116
- Installing ri documentation for sisimai-5.0.1
112
+ Fetching: sisimai-5.0.2.gem (100%)
113
+ Successfully installed sisimai-5.0.2
114
+ Parsing documentation for sisimai-5.0.2
115
+ Installing ri documentation for sisimai-5.0.2
117
116
  Done installing documentation for sisimai after 6 seconds
118
117
  1 gem installed
119
118
  ```
@@ -141,13 +140,13 @@ if [ -d "/usr/local/jr" ]; then \
141
140
  ...
142
141
  3 gems installed
143
142
  /opt/local/bin/rake install
144
- sisimai 5.0.0 built to pkg/sisimai-5.0.0.gem.
145
- sisimai (5.0.0) installed.
143
+ sisimai 5.0.2 built to pkg/sisimai-5.0.2.gem.
144
+ sisimai (5.0.2) installed.
146
145
  if [ -d "/usr/local/jr" ]; then \
147
146
  PATH="/usr/local/jr/bin:$PATH" /usr/local/jr/bin/rake install; \
148
147
  fi
149
- sisimai 5.0.0 built to pkg/sisimai-5.0.0-java.gem.
150
- sisimai (5.0.0) installed.
148
+ sisimai 5.0.2 built to pkg/sisimai-5.0.2-java.gem.
149
+ sisimai (5.0.2) installed.
151
150
  ```
152
151
 
153
152
  Usage
@@ -348,15 +347,15 @@ Beginning with v5.0.0, Sisimai requires **Ruby 2.4.0 or later.**
348
347
 
349
348
  | Features | Sisimai 4 | Sisimai 5 |
350
349
  |------------------------------------------------------|--------------------|---------------------|
351
- | System requirements (CRuby) | 2.1 - 3.3.0 | **2.4** - 3.3.0 |
352
- | System requirements (JRuby) | 9.0.4.0 - 9.1.17.0 | 9.0.4.0 - 9.1.17.0 |
350
+ | System requirements (CRuby) | 2.1 - 3.3.0 | **2.4** or later |
351
+ | System requirements (JRuby) | 9.0.4.0 - 9.1.17.0 | **9.2** or later |
353
352
  | Callback feature for the original email file | N/A | Available[^3] |
354
353
  | The number of MTA/ESP modules | 68 | 70 |
355
354
  | The number of detectable bounce reasons | 29 | 34 |
356
355
  | Dependencies (Except Ruby Standard Gems) | 1 gem | 1 gem |
357
- | Source lines of code | 10,300 lines | 11,300 lines |
356
+ | Source lines of code | 10,300 lines | 11,370 lines |
358
357
  | Test frameworks | rspec | minitest |
359
- | The number of tests in spec/ or test/ directory | 311,000 tests | 336,000 tests |
358
+ | The number of tests in spec/ or test/ directory | 311,000 tests | 338,000 tests |
360
359
  | The number of bounce emails decoded/sec (CRuby)[^4] | 231 emails | 305 emails |
361
360
  | License | 2 Clause BSD | 2 Caluse BSD |
362
361
  | Commercial support | Available | Available |
data/lib/sisimai/fact.rb CHANGED
@@ -46,6 +46,18 @@ module Sisimai
46
46
  RFC822Head = Sisimai::RFC5322.HEADERFIELDS(:all)
47
47
  ActionList = { delayed: 1, delivered: 1, expanded: 1, failed: 1, relayed: 1 };
48
48
 
49
+ if RUBY_PLATFORM.start_with?('java')
50
+ # [WORKAROUND] #159 #267 JRuby seems to fail and throws exception at strptime(), but this
51
+ # issue might be fixed in a future version of JRuby.
52
+ # https://gist.github.com/hiroyuki-sato/6ef40245874d4c847a95ef99886e4fa7
53
+ # https://github.com/sisimai/rb-sisimai/issues/267#issuecomment-1976642884
54
+ # https://github.com/jruby/jruby/issues/8139
55
+ # https://github.com/sisimai/rb-sisimai/issues/267
56
+ TimeModule = ::DateTime
57
+ else
58
+ TimeModule = Sisimai::Time
59
+ end
60
+
49
61
  # Constructor of Sisimai::Fact
50
62
  # @param [Hash] argvs Including each parameter
51
63
  # @return [Sisimai::Fact] Structured email data
@@ -188,7 +200,7 @@ module Sisimai
188
200
 
189
201
  begin
190
202
  # Convert from the date string to an object then calculate time zone offset.
191
- t = Sisimai::Time.strptime(datestring, '%a, %d %b %Y %T')
203
+ t = TimeModule.strptime(datestring, '%a, %d %b %Y %T')
192
204
  p['timestamp'] = (t.to_time.to_i - zoneoffset) || nil
193
205
  rescue
194
206
  warn ' ***warning: Failed to strptime ' << datestring.to_s
@@ -196,12 +208,12 @@ module Sisimai
196
208
  next unless p['timestamp']
197
209
 
198
210
  # OTHER_TEXT_HEADERS:
199
- recvheader = mesg1['header']['received'] || []
200
- unless recvheader.empty?
201
- # Get localhost and remote host name from Received header.
202
- %w[lhost rhost].each { |a| e[a] ||= '' }
203
- e['lhost'] = Sisimai::RFC5322.received(recvheader[0]).shift if e['lhost'].empty?
204
- e['rhost'] = Sisimai::RFC5322.received(recvheader[-1]).pop if e['rhost'].empty?
211
+ rr = mesg1['header']['received'] || []
212
+ unless rr.empty?
213
+ # Get a localhost and a remote host name from Received header.
214
+ p['rhost'] = Sisimai::RFC5322.received(rr[-1])[1] if p['rhost'].empty?
215
+ p['lhost'] = '' if p['rhost'] == p['lhost']
216
+ p['lhost'] = Sisimai::RFC5322.received(rr[ 0])[0] if p['lhost'].empty?
205
217
  end
206
218
 
207
219
  # Remove square brackets and curly brackets from the host variable
@@ -339,9 +351,35 @@ module Sisimai
339
351
  o['catch'] = p['catch'] || nil
340
352
  o['hardbounce'] = p['hardbounce']
341
353
  o['replycode'] = Sisimai::SMTP::Reply.find(p['diagnosticcode']).to_s if o['replycode'].empty?
342
- o['timestamp'] = Sisimai::Time.parse(::Time.at(p['timestamp']).to_s)
354
+ o['timestamp'] = TimeModule.parse(::Time.at(p['timestamp']).to_s)
343
355
  o['timezoneoffset'] = p['timezoneoffset'] || '+0000'
344
356
 
357
+ # ALIAS
358
+ while true do
359
+ # Look up the Envelope-To address from the Received: header in the original message
360
+ # when the recipient address is same with the value of o['alias'].
361
+ break if o['alias'].empty?
362
+ break if o['recipient'].address != o['alias']
363
+ break unless rfc822data.has_key?('received')
364
+ break if rfc822data['received'].empty?
365
+
366
+ rfc822data['received'].reverse.each do |er|
367
+ # Search for the string " for " from the Received: header
368
+ next unless er.include?(' for ')
369
+
370
+ af = Sisimai::RFC5322.received(er)
371
+ next if af.empty?
372
+ next if af[5].empty?
373
+ next unless Sisimai::Address.is_emailaddress(af[5])
374
+ next if o['recipient'].address == af[5]
375
+
376
+ o['alias'] = af[5]
377
+ break
378
+ end
379
+ break
380
+ end
381
+ o['alias'] = '' if o['alias'] == o['recipient'].address
382
+
345
383
  # REASON: Decide the reason of email bounce
346
384
  if o['reason'].empty? || RetryIndex[o['reason']]
347
385
  # The value of "reason" is empty or is needed to check with other values again
@@ -305,7 +305,6 @@ module Sisimai::Lhost
305
305
 
306
306
  dscontents.each do |e|
307
307
  # Set default values if each value is empty.
308
- e['lhost'] ||= permessage['rhost']
309
308
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
310
309
 
311
310
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'].to_s.tr("\n", ' '))
@@ -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
 
104
103
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
@@ -100,7 +100,6 @@ module Sisimai::Lhost
100
100
 
101
101
  dscontents.each do |e|
102
102
  # Set default values if each value is empty.
103
- e['lhost'] ||= permessage['rhost']
104
103
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
105
104
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'].tr("\n", ' '))
106
105
 
@@ -108,7 +108,6 @@ module Sisimai::Lhost
108
108
 
109
109
  dscontents.each do |e|
110
110
  # Set default values if each value is empty.
111
- e['lhost'] ||= permessage['rhost']
112
111
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
113
112
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
114
113
  e['command'] = thecommand || ''
@@ -123,7 +123,6 @@ module Sisimai::Lhost
123
123
  dscontents.each do |e|
124
124
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
125
125
  e['recipient'] = Sisimai::Address.s3s4(e['recipient'])
126
- e['lhost'] ||= permessage['rhost']
127
126
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
128
127
 
129
128
  MessagesOf.each_key do |r|
@@ -147,7 +147,7 @@ module Sisimai::Lhost
147
147
  next unless p > 0
148
148
 
149
149
  # #550 5.1.1 RESOLVER.ADR.RecipNotFound; not found ##
150
- f = e['diagnosis'][p + 1, e['diagnosis'].index(';') - p -1]
150
+ f = e['diagnosis'][p + 1, e['diagnosis'].index(';') - p - 1]
151
151
  NDRSubject.each_key do |r|
152
152
  # Try to match with error subject strings
153
153
  next unless f == r
@@ -147,7 +147,6 @@ module Sisimai::Lhost
147
147
  readcursor = 0 # (Integer) Points the current cursor position
148
148
  nextcursor = 0
149
149
  recipients = 0 # (Integer) The number of 'Final-Recipient' header
150
- localhost0 = '' # (String) Local MTA
151
150
  boundary00 = '' # (String) Boundary string
152
151
  v = nil
153
152
 
@@ -342,18 +341,13 @@ module Sisimai::Lhost
342
341
  end
343
342
  return nil unless recipients > 0
344
343
 
345
- unless mhead['received'].empty?
346
- # Get the name of local MTA
347
- # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128])
348
- p1 = mhead['received'][-1].index('from ') || -1
349
- p2 = mhead['received'][-1].index(' ', p1 + 5) || -1
350
- localhost0 = mhead['received'][-1][p1 + 5, p2 - p1 - 5] if p1 > -1 && p2 > -1
351
- end
344
+ # Get the name of the local MTA
345
+ # Received: from marutamachi.example.org (c192128.example.net [192.0.2.128])
346
+ receivedby = mhead['received'] || []
347
+ recvdtoken = Sisimai::RFC5322.received(receivedby[-1])
352
348
 
353
349
  dscontents.each do |e|
354
- # Set default values if each value is empty.
355
- e['lhost'] ||= localhost0
356
-
350
+ # Check the error message, the rhost, the lhost, and the smtp command.
357
351
  unless e['diagnosis']
358
352
  # Empty Diagnostic-Code: or error message
359
353
  unless boundary00.empty?
@@ -405,12 +399,9 @@ module Sisimai::Lhost
405
399
  p1 = e['diagnosis'].index('host ') || -1
406
400
  p2 = e['diagnosis'].index(' ', p1 + 5) || -1
407
401
  e['rhost'] = e['diagnosis'][p1 + 5, p2 - p1 - 5] if p1 > -1
408
-
409
- unless e['rhost']
410
- # Get localhost and remote host name from Received header.
411
- e['rhost'] = Sisimai::RFC5322.received(mhead['received'][-1]).pop unless mhead['received'].empty?
412
- end
402
+ e['rhost'] ||= recvdtoken[1]
413
403
  end
404
+ e['lhost'] ||= recvdtoken[0]
414
405
 
415
406
  unless e['command']
416
407
  # Get the SMTP command name for the session
@@ -146,7 +146,6 @@ module Sisimai::Lhost
146
146
  return nil unless recipients > 0
147
147
 
148
148
  dscontents.each do |e|
149
- e['lhost'] ||= permessage['lhost']
150
149
  e['diagnosis'] = Sisimai::String.sweep(e['diagnosis'])
151
150
 
152
151
  p0 = e['diagnosis'].index('-') || -1
@@ -48,9 +48,10 @@ module Sisimai::Lhost
48
48
  # * You may need to join the group before receiving permission to post.
49
49
  # * This group may not be open to posting.
50
50
  entiremesg = emailparts[0].split(/\n\n/, 5).slice(0, 4).join(' ').tr("\n", ' ');
51
+ receivedby = mhead['received'] || []
51
52
  recordwide['diagnosis'] = Sisimai::String.sweep(entiremesg)
52
53
  recordwide['reason'] = emailparts[0].scan(/^[ ]?[*][ ]?/).size == 4 ? 'rejected' : 'onhold'
53
- recordwide['rhost'] = Sisimai::RFC5322.received(mhead['received'][0]).shift
54
+ recordwide['rhost'] = Sisimai::RFC5322.received(receivedby[0])[1]
54
55
 
55
56
  mhead['x-failed-recipients'].split(',').each do |e|
56
57
  # X-Failed-Recipients: neko@example.jp, nyaan@example.org, ...
@@ -135,7 +135,6 @@ module Sisimai::Lhost
135
135
 
136
136
  dscontents.each do |e|
137
137
  # Set default values if each value is empty.
138
- e['lhost'] ||= permessage['rhost']
139
138
  permessage.each_key { |a| e[a] ||= permessage[a] || '' }
140
139
 
141
140
  if anotherset['diagnosis']
@@ -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?
@@ -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',
@@ -78,6 +79,7 @@ module Sisimai
78
79
  ['message from ', ' rejected based on blacklist'],
79
80
  ['messages from ', ' temporarily deferred due to user complaints'], # Yahoo!
80
81
  ['server ip ', ' listed as abusive'],
82
+ ['sorry! your ip address', ' is blocked by rbl'], # junkemailfilter.com
81
83
  ['the domain ', ' is blacklisted'],
82
84
  ['the email ', ' is blacklisted'],
83
85
  ['the ip', ' is blacklisted'],
@@ -18,6 +18,7 @@ module Sisimai
18
18
  'has been delayed',
19
19
  'it has not been collected after',
20
20
  'message expired after sitting in queue for',
21
+ 'message expired, cannot connect to remote server',
21
22
  'message expired, connection refulsed',
22
23
  'message timed out',
23
24
  'retry time not reached for any host after a long failure period',
@@ -15,6 +15,7 @@ module Sisimai
15
15
  'boite du destinataire pleine',
16
16
  'delivery failed: over quota',
17
17
  'disc quota exceeded',
18
+ 'diskspace quota',
18
19
  'does not have enough space',
19
20
  'exceeded storage allocation',
20
21
  'exceeding its mailbox quota',
@@ -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',
@@ -21,6 +21,7 @@ module Sisimai
21
21
  'recipient rejected: temporarily inactive',
22
22
  'recipient suspend the service',
23
23
  'this account has been disabled or discontinued',
24
+ 'this address no longer accepts mail',
24
25
  'this mailbox is disabled',
25
26
  'user suspended', # http://mail.163.com/help/help_spam_16.htm
26
27
  'vdelivermail: account is locked email bounced',
@@ -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
@@ -7,6 +7,7 @@ module Sisimai
7
7
  class << self
8
8
  MessagesOf = {
9
9
  # https://community.mimecast.com/s/article/Mimecast-SMTP-Error-Codes-842605754
10
+ # https://community.mimecast.com/s/article/email-security-cloud-gateway-mimecast-smtp-error-codes
10
11
  'authfailure' => [
11
12
  # - The inbound message has been rejected because the originated IP address isn't list-
12
13
  # ed in the published SPF records for the sending domain.
@@ -76,6 +77,12 @@ module Sisimai
76
77
  [554, 'maximum email size exceeded'],
77
78
  ],
78
79
  'networkerror' => [
80
+ # - The recipients' domains have MX records configured incorrectly
81
+ # - Check and remove any MX records that point to hostnames with outbound references.
82
+ # Only Inbound smart hosts are supported on MX records.
83
+ [451, 'the incorrect hostname used for inbounds'],
84
+ [550, 'the incorrect hostname used for inbounds'],
85
+
79
86
  # - The message has too many "received headers" as it has been forwarded across multi-
80
87
  # ple hops. Once 25 hops have been reached, the email is rejected.
81
88
  # - Investigate the email addresses in the communication pairs, to see what forwarders
@@ -135,8 +142,8 @@ module Sisimai
135
142
 
136
143
  # - A personal block policy is in place for the email address/domain.
137
144
  # - Remove the email address/domain from the Managed Senders list.
138
- [550, 'envelope blocked user entry'],
139
- [550, 'envelope blocked user domain entry'],
145
+ [550, 'envelope blocked - user entry'],
146
+ [550, 'envelope blocked - user domain entry'],
140
147
  [550, 'rejected by header-based manually blocked senders - block for manual block'],
141
148
 
142
149
  # - A Block Sender Policy has been applied to reject emails based on the Header From or
@@ -1,4 +1,4 @@
1
1
  # Define the version number of Sisimai
2
2
  module Sisimai
3
- VERSION = '5.0.1'.freeze
3
+ VERSION = '5.0.2'.freeze
4
4
  end
@@ -0,0 +1,85 @@
1
+ Return-Path: <>
2
+ X-Original-To: nekochan@email.example.jp
3
+ Delivered-To: mail@email.example.jp
4
+ Received: from mx311.ume.example.ne.jp (ip-192-0-2-25.us-east-1.compute.internal [192.0.2.25])
5
+ by mx1.email.example.jp (Postfix) with ESMTPS id Ln2ZS7LPwxzW4HMF
6
+ for <nekochan@email.example.jp>; Wed, 7 Feb 2024 23:34:45 +0900 (JST)
7
+ Received: from localhost (localhost)
8
+ by mx311.ume.example.ne.jp (8.16.1/8.16.1) id 3LGub1et091679;
9
+ Wed, 7 Feb 2024 23:34:45 +0900 (JST)
10
+ (envelope-from MAILER-DAEMON)
11
+ Date: Wed, 7 Feb 2024 23:34:45 +0900 (JST)
12
+ From: Mail Delivery Subsystem <MAILER-DAEMON@mx311.ume.example.ne.jp>
13
+ To: <nekochan@email.example.jp>
14
+ Message-Id: <202402072222.3LGub1et091679@mx311.ume.example.ne.jp>
15
+ MIME-Version: 1.0
16
+ Content-Type: multipart/report; report-type=delivery-status;
17
+ boundary="3LGub1et091679.1157497350/mx311.ume.example.ne.jp"
18
+ Subject: Returned mail: see transcript for details
19
+ Auto-Submitted: auto-generated (failure)
20
+
21
+ This is a MIME-encapsulated message
22
+
23
+ --3LGub1et091679.1157497350/mx311.ume.example.ne.jp
24
+
25
+ The original message was received at Wed, 7 Feb 2024 23:34:45 +0900 (JST)
26
+ from localhost [127.0.0.1]
27
+
28
+ ----- The following addresses had permanent fatal errors -----
29
+ <kijitora-cat@google.example.com>
30
+ (reason: 550-5.7.26 The MAIL FROM domain [email.example.jp] has an SPF record with a hard fail)
31
+
32
+ ----- Transcript of session follows -----
33
+ ... while talking to gmail-smtp-in.l.google.com.:
34
+ >>> DATA
35
+ <<< 550-5.7.26 The MAIL FROM domain [email.example.jp] has an SPF record with a hard fail
36
+ <<< 550-5.7.26 policy (-all) but it fails to pass SPF checks with the ip:
37
+ <<< 550-5.7.26 [203.0.113.22]. To best protect our users from spam and phishing,
38
+ <<< 550-5.7.26 the message has been blocked. For instructions on setting up
39
+ <<< 550-5.7.26 authentication, go to
40
+ <<< 550 5.7.26 https://support.google.com/mail/answer/81126#authentication q2.22 - gsmtp
41
+ 554 5.0.0 Service unavailable
42
+
43
+ --3LGub1et091679.1157497350/mx311.ume.example.ne.jp
44
+ Content-Type: message/delivery-status
45
+
46
+ Reporting-MTA: dns; mx311.ume.example.ne.jp
47
+ Arrival-Date: Wed, 7 Feb 2024 23:34:45 +0900 (JST)
48
+
49
+ Final-Recipient: RFC822; kijitora-cat@google.example.com
50
+ X-Actual-Recipient: rfc822; kijitora-cat@google.example.com
51
+ Action: failed
52
+ Status: 5.7.26
53
+ Remote-MTA: DNS; gmail-smtp-in.l.google.com
54
+ Diagnostic-Code: SMTP; 550-5.7.26 The MAIL FROM domain [email.example.jp] has an SPF record with a hard fail
55
+ Last-Attempt-Date: Wed, 7 Feb 2024 23:34:45 +0900 (JST)
56
+
57
+ --3LGub1et091679.1157497350/mx311.ume.example.ne.jp
58
+ Content-Type: message/rfc822
59
+
60
+ Return-Path: <nekochan@email.example.jp>
61
+ Received: from mx311.ume.example.ne.jp (localhost [127.0.0.1])
62
+ by mx311.ume.example.ne.jp (8.16.1/8.16.1) with ESMTP id ujmeV2ZG033926
63
+ for <kijitora-cat@google.example.com>; Wed, 7 Feb 2024 23:34:45 +0900 (JST)
64
+ (envelope-from nekochan@email.example.jp)
65
+ Received: (from kijitora@localhost)
66
+ by mx311.ume.example.ne.jp (8.16.1/8.16.1/Submit) id BJObadmZ060489
67
+ for kijitora-cat@google.example.com; Wed, 7 Feb 2024 23:34:45 +0900 (JST)
68
+ (envelope-from nekochan@email.example.jp)
69
+ Received: from relay1.email.example.jp (relay1.email.example.jp [192.168.168.168])
70
+ by mx311.ume.example.ne.jp (8.16.1/8.16.1) with ESMTPS id Mf73VCB9P8z4lM3b
71
+ (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NO)
72
+ for <neko@libsisimai.org>; Wed, 7 Feb 2024 23:34:45 +0900 (JST)
73
+ (envelope-from nekochan@email.example.jp)
74
+ Content-Type: text/plain; charset=us-ascii
75
+ Message-Id: <50ad190.20240207.2202@relay1.email.example.jp>
76
+ Content-Transfer-Encoding: 7bit
77
+ Subject: Nyaan
78
+ From: <nekochan@email.example.jp>
79
+ To: <neko@libsisimai.org>
80
+ Date: 7 Feb 2024 23:34:45 +0900
81
+ MIME-Version: 1.0
82
+
83
+ Nyaan?
84
+
85
+ --3LGub1et091679.1157497350/mx311.ume.example.ne.jp--
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sisimai
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.1
4
+ version: 5.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - azumakuniyuki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-03 00:00:00.000000000 Z
11
+ date: 2024-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -74,6 +74,7 @@ executables: []
74
74
  extensions: []
75
75
  extra_rdoc_files: []
76
76
  files:
77
+ - ".github/workflows/rake-test.yml"
77
78
  - ".gitignore"
78
79
  - ".rubocop.yml"
79
80
  - ".travis.yml"
@@ -640,6 +641,7 @@ files:
640
641
  - set-of-emails/maildir/bsd/lhost-sendmail-57.eml
641
642
  - set-of-emails/maildir/bsd/lhost-sendmail-58.eml
642
643
  - set-of-emails/maildir/bsd/lhost-sendmail-59.eml
644
+ - set-of-emails/maildir/bsd/lhost-sendmail-60.eml
643
645
  - set-of-emails/maildir/bsd/lhost-surfcontrol-01.eml
644
646
  - set-of-emails/maildir/bsd/lhost-surfcontrol-02.eml
645
647
  - set-of-emails/maildir/bsd/lhost-surfcontrol-03.eml