sisimai 4.22.0 → 4.22.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sisimai might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 78125bd98eedb70240490fd0dfef3e3072679204
4
- data.tar.gz: c384d1e57686bd86686338c5877ee1197ffb4272
3
+ metadata.gz: c644b2582fd163b4227e472c750409fff659c691
4
+ data.tar.gz: 39c38f411e6411a77f907d96877c808a8f731879
5
5
  SHA512:
6
- metadata.gz: 022a3627ae60a191f1eb641f2d5dac85672806a0453b99cbc19b27d5057a2845054dae8f4b7bbd441199c9617e5660de8b9dffbc4fb898903b3bdecd7af5b397
7
- data.tar.gz: 7d294183cfd12dbe5e8dbf1de142ffd94897039d12ec0f5d0bd9ac200a588ea7aa9bda7ac31f6fdd695a87852d9695d5dceb5ccbe3e80aa6741936d1f73b05f5
6
+ metadata.gz: 8d08ac534acade16e7aedd5ef71bd12bd44702b51283833a874e1f178f4e3e5e7e6bab3f8544e0e1254e686f8c4455e15b397bbd05a89a2aba343202013a818e
7
+ data.tar.gz: 2302808a843715d2a463256abde74c8dd208b277d4325941b39e64ad87f429252e65f794e7a30fd8a924e2d665b77e7026088cbd974a9fcfc29b61f67c5e45be
data/.travis.yml CHANGED
@@ -5,8 +5,8 @@ rvm:
5
5
  - 2.2.4
6
6
  - 2.3.0
7
7
  - 2.4.0
8
- - jruby-9.0.4.0
9
8
  - jruby-9.0.5.0
9
+ - jruby-9.1.9.0
10
10
  before_install:
11
11
  script:
12
12
  - rake spec
data/ChangeLog.md CHANGED
@@ -3,6 +3,26 @@ 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
+ v4.22.1
7
+ --------------------------------------------------------------------------------
8
+ - release: "Tue, 29 Aug 2017 17:25:22 +0900 (JST)"
9
+ - version: "4.22.1"
10
+ - changes:
11
+ - Sisimai::Address was born again: import Pull-Request sisimai/p5-Sisimai#231
12
+ - Implement new email address parser method: find()
13
+ - Implement new constructor: make()
14
+ - Implement new writable accessors: name() and comment()
15
+ - parse() method was marked as obsoleted
16
+ - **Require Oj 3.0.0 or later. Build test fails when the version of installed
17
+ Oj is 2.18.* on CRuby.**
18
+ - Tested on JRuby 9.1.9.0.
19
+ - Fix wrong constant name in Sisimai::Rhost::ExchangeOnline reported at issue
20
+ #77. Thanks to @rdeavila.
21
+ - Improved code in Sisimai::Message::Email to avoid an exception reported at
22
+ issue #82. Thanks to @hiroyuki-sato.
23
+ - Fixed wrong bitwise operation in Sisimai::RFC3464 for getting the original
24
+ message part Thanks to @hiroyuki-sato.
25
+
6
26
  v4.22.0
7
27
  --------------------------------------------------------------------------------
8
28
  - release: "Tue, 22 Aug 2017 18:25:55 +0900 (JST)"
data/README-JA.md CHANGED
@@ -239,8 +239,8 @@ Differences between Ruby version and Perl version
239
239
  | メール解析速度(1000通のメール) | 3.30秒 | 2.33秒 |
240
240
  | インストール方法 | gem install | cpanm, cpm |
241
241
  | 依存モジュール数(コアモジュールを除く) | 1モジュール | 2モジュール |
242
- | LOC:ソースコードの行数 | 12400行 | 9100行 |
243
- | テスト件数(spec/,t/,xt/ディレクトリ) | 205700件 | 202400件 |
242
+ | LOC:ソースコードの行数 | 12600行 | 9300行 |
243
+ | テスト件数(spec/,t/,xt/ディレクトリ) | 196800件 | 204600件 |
244
244
  | ライセンス | 二条項BSD | 二条項BSD |
245
245
  | 開発会社によるサポート契約 | 準備中 | 提供中 |
246
246
 
data/README.md CHANGED
@@ -243,8 +243,8 @@ and bounceHammer are available at
243
243
  | The speed of parsing email(1000 emails) | 3.30s | 2.33s |
244
244
  | How to install | gem install | cpanm, cpm |
245
245
  | Dependencies (Except core modules) | 1 module | 2 modules |
246
- | LOC:Source lines of code | 12400 lines | 9100 lines |
247
- | The number of tests(spec/,t/,xt/) directory | 205700 tests | 202400 tests |
246
+ | LOC:Source lines of code | 12600 lines | 9300 lines |
247
+ | The number of tests(spec/,t/,xt/) directory | 196800 tests | 204600 tests |
248
248
  | License | BSD 2-Clause | BSD 2-Clause |
249
249
  | Support Contract provided by Developer | Coming soon | Available |
250
250
 
@@ -16,75 +16,102 @@ module Sisimai
16
16
  return sprintf('undisclosed-%s-in-headers@libsisimai.org.invalid', local)
17
17
  end
18
18
 
19
- def self.find(argvs, addrs = nil)
19
+ # New constructor of Sisimai::Address
20
+ # @param [Hash] argvs Email address, name, and other elements
21
+ # @return [Sisimai::Address] Object or Undef when the email address was
22
+ # not valid.
23
+ # @example make({address: 'neko@example.org', name: 'Neko', comment: '(nyaan)')}
24
+ # # => Sisimai::Address object
25
+ def self.make(argvs)
26
+ return nil unless argvs.is_a? Hash
27
+ return nil unless argvs.key?(:address)
28
+ return nil if argvs[:address].empty?
29
+
30
+ thing = Sisimai::Address.new(argvs[:address])
31
+ return nil unless thing
32
+ return nil if thing.void
33
+
34
+ thing.name = argvs[:name] || ''
35
+ thing.comment = argvs[:comment] || ''
36
+
37
+ return thing
38
+ end
39
+
40
+ def self.find(argv1 = nil, addrs = false)
20
41
  # Email address parser with a name and a comment
21
- # @param [String] argvs String including email address
42
+ # @param [String] argv1 String including email address
22
43
  # @param [Boolean] addrs true: Returns list including all the elements
23
44
  # false: Returns list including email addresses only
24
45
  # @return [Array, Nil] Email address list or Undef when there is no
25
46
  # email address in the argument
26
47
  # @example Parse email address
27
- # input: parse('Neko <neko@example.cat>')
28
- # output: [{'address' => 'neko@example.cat', 'name' => 'Neko'}]
29
- return nil unless argvs
30
- argvs = argvs.gsub(/[\r\n]/, '')
48
+ # find('Neko <neko(nyaan)@example.org>')
49
+ # #=> [{ address: 'neko@example.org', name: 'Neko', comment: '(nyaan)'}]
50
+ return nil unless argv1
51
+ argv1 = argv1.gsub(/[\r\n]/, '')
31
52
 
32
- emailtable = { 'address' => '', 'name' => '', 'comment' => '' }
53
+ emailtable = { address: '', name: '', comment: '' }
33
54
  addrtables = []
55
+ readbuffer = []
34
56
  readcursor = 0
35
57
  delimiters = ['<', '>', '(', ')', '"', ',']
58
+ validemail = %r{(?>
59
+ (?:([^\s]+|["].+?["])) # local part
60
+ [@]
61
+ (?:([^@\s]+|[0-9A-Za-z:\.]+)) # domain part
62
+ )
63
+ }x
36
64
  indicators = {
37
65
  :'email-address' => (1 << 0), # <neko@example.org>
38
66
  :'quoted-string' => (1 << 1), # "Neko, Nyaan"
39
67
  :'comment-block' => (1 << 2), # (neko)
40
68
  }
41
- characters = argvs.split('')
42
- readbuffer = []
43
69
 
44
70
  v = emailtable # temporary buffer
45
71
  p = '' # current position
46
72
 
47
- characters.each do |e|
73
+ argv1.split('').each do |e|
48
74
  # Check each characters
75
+
49
76
  if delimiters.detect { |r| r == e }
50
77
  # The character is a delimiter character
51
78
  if e == ','
52
79
  # Separator of email addresses or not
53
- if v['address'] =~ /\A[<].+[@].+[>]\z/
80
+ if v[:address] =~ /\A[<].+[@].+[>]\z/
54
81
  # An email address has already been picked
55
82
 
56
83
  if readcursor & indicators[:'comment-block'] > 0
57
84
  # The cursor is in the comment block (Neko, Nyaan)
58
- v['comment'] += e
85
+ v[:comment] += e
59
86
 
60
87
  elsif readcursor & indicators[:'quoted-string'] > 0
61
88
  # "Neko, Nyaan"
62
- v['name'] += e
89
+ v[:name] += e
63
90
 
64
91
  else
65
92
  # The cursor is not in neither the quoted-string nor the comment block
66
93
  readcursor = 0 # reset cursor position
67
94
  readbuffer << v
68
- v = { 'address' => '', 'name' => '', 'comment' => '' }
95
+ v = { address: '', name: '', comment: '' }
69
96
  p = ''
70
97
  end
71
98
  else
72
99
  # "Neko, Nyaan" <neko@nyaan.example.org> OR <"neko,nyaan"@example.org>
73
- p.size > 0 ? (v[p] += e) : (v['name'] += e)
100
+ p.size > 0 ? (v[p] += e) : (v[:name] += e)
74
101
  end
75
102
  next
76
103
  end # End of if(',')
77
104
 
78
105
  if e == '<'
79
106
  # <: The beginning of an email address or not
80
- if v['address'].size > 0
81
- p.size > 0 ? (v[p] += e) : (v['name'] += e)
107
+ if v[:address].size > 0
108
+ p.size > 0 ? (v[p] += e) : (v[:name] += e)
82
109
 
83
110
  else
84
111
  # <neko@nyaan.example.org>
85
112
  readcursor |= indicators[:'email-address']
86
- v['address'] += e
87
- p = 'address'
113
+ v[:address] += e
114
+ p = :address
88
115
  end
89
116
  next
90
117
  end
@@ -95,11 +122,11 @@ module Sisimai
95
122
  if readcursor & indicators[:'email-address'] > 0
96
123
  # <neko@example.org>
97
124
  readcursor &= ~indicators[:'email-address']
98
- v['address'] += e
125
+ v[:address] += e
99
126
  p = ''
100
127
  else
101
128
  # a comment block or a display name
102
- p.size > 0 ? (v['comment'] += e) : (v['name'] += e)
129
+ p.size > 0 ? (v[:comment] += e) : (v[:name] += e)
103
130
  end
104
131
  next
105
132
  end # End of if('>')
@@ -108,29 +135,32 @@ module Sisimai
108
135
  # The beginning of a comment block or not
109
136
  if readcursor & indicators[:'email-address'] > 0
110
137
  # <"neko(nyaan)"@example.org> or <neko(nyaan)@example.org>
111
- if v['address'] =~ /["]/
138
+ if v[:address] =~ /["]/
112
139
  # Quoted local part: <"neko(nyaan)"@example.org>
113
- v['address'] += e
140
+ v[:address] += e
114
141
 
115
142
  else
116
143
  # Comment: <neko(nyaan)@example.org>
117
144
  readcursor |= indicators[:'comment-block']
118
- v['comment'] += e
119
- p = 'comment'
145
+ v[:comment] += ' ' if v[:comment] =~ /[)]\z/
146
+ v[:comment] += e
147
+ p = :comment
120
148
  end
121
149
  elsif readcursor & indicators[:'comment-block'] > 0
122
150
  # Comment at the outside of an email address (...(...)
123
- v['comment'] += e
151
+ v[:comment] += ' ' if v[:comment] =~ /[)]\z/
152
+ v[:comment] += e
124
153
 
125
154
  elsif readcursor & indicators[:'quoted-string'] > 0
126
155
  # "Neko, Nyaan(cat)", Deal as a display name
127
- v['name'] += e
156
+ v[:name] += e
128
157
 
129
158
  else
130
159
  # The beginning of a comment block
131
160
  readcursor |= indicators[:'comment-block']
132
- v['comment'] += e
133
- p = 'comment'
161
+ v[:comment] += ' ' if v[:comment] =~ /[)]\z/
162
+ v[:comment] += e
163
+ p = :comment
134
164
  end
135
165
  next
136
166
  end # End of if('(')
@@ -139,26 +169,26 @@ module Sisimai
139
169
  # The end of a comment block or not
140
170
  if readcursor & indicators[:'email-address'] > 0
141
171
  # <"neko(nyaan)"@example.org> OR <neko(nyaan)@example.org>
142
- if v['address'] =~ /["]/
172
+ if v[:address] =~ /["]/
143
173
  # Quoted string in the local part: <"neko(nyaan)"@example.org>
144
- v['address'] += e
174
+ v[:address] += e
145
175
 
146
176
  else
147
177
  # Comment: <neko(nyaan)@example.org>
148
178
  readcursor &= ~indicators[:'comment-block']
149
- v['comment'] += e
150
- p = 'address'
179
+ v[:comment] += e
180
+ p = :address
151
181
  end
152
182
  elsif readcursor & indicators[:'comment-block'] > 0
153
183
  # Comment at the outside of an email address (...(...)
154
184
  readcursor &= ~indicators[:'comment-block']
155
- v['comment'] += e
185
+ v[:comment] += e
156
186
  p = ''
157
187
 
158
188
  else
159
189
  # Deal as a display name
160
190
  readcursor &= ~indicators[:'comment-block']
161
- v['name'] = e
191
+ v[:name] = e
162
192
  p = ''
163
193
  end
164
194
  next
@@ -171,92 +201,87 @@ module Sisimai
171
201
  v[p] += e
172
202
  else
173
203
  # Display name
174
- v['name'] += e
204
+ v[:name] += e
175
205
  if readcursor & indicators[:'quoted-string'] > 0
176
206
  # "Neko, Nyaan"
177
- unless v['name'] =~ /\x5c["]\z/
207
+ unless v[:name] =~ /\x5c["]\z/
178
208
  # "Neko, Nyaan \"...
179
209
  readcursor &= ~indicators[:'quoted-string']
180
210
  p = ''
181
211
  end
182
- else
183
- if readcursor & indicators[:'email-address'] == 0 &&
184
- readcursor & indicators[:'comment-block'] == 0
185
- # Deal as the beginning of a display name
186
- readcursor |= indicators[:'quoted-string']
187
- p = 'name'
188
- end
189
212
  end
190
213
  end
191
214
  next
192
215
  end # End of if('"')
193
216
  else
194
217
  # The character is not a delimiter
195
- p.size > 0 ? (v[p] += e) : (v['name'] += e)
218
+ p.size > 0 ? (v[p] += e) : (v[:name] += e)
196
219
  next
197
220
  end
198
221
  end
199
222
 
200
- if v['address'].size > 0
223
+ if v[:address].size > 0
201
224
  # Push the latest values
202
225
  readbuffer << v
203
226
  else
204
227
  # No email address like <neko@example.org> in the argument
205
- if v['name'] =~ /[@]/
228
+ if cv = v[:name].match(validemail)
206
229
  # String like an email address will be set to the value of "address"
207
- if cv = v['name'].match(/(["].+?["][@][^ ]+)/)
208
- # "neko nyaan"@example.org
209
- v['address'] = cv[1]
230
+ v[:address] = sprintf("%s@%s", cv[1], cv[2])
210
231
 
211
- elsif cv = v['name'].match(/([^\s]+[@][^\s]+)/)
212
- # neko-nyaan@example.org
213
- v['address'] = cv[1]
214
- end
215
- elsif Sisimai::RFC5322.is_mailerdaemon(v['name'])
232
+ elsif Sisimai::RFC5322.is_mailerdaemon(v[:name])
216
233
  # Allow if the argument is MAILER-DAEMON
217
- v['address'] = v['name']
234
+ v[:address] = v[:name]
218
235
  end
219
236
 
220
- if v['address'].size > 0
221
- # Remove the value of "name" and remove the comment from the address
222
- if cv = v['address'].match(/(.*)([(].+[)])(.*)/)
237
+ if v[:address].size > 0
238
+ # Remove the comment from the address
239
+ if cv = v[:address].match(/(.*)([(].+[)])(.*)/)
223
240
  # (nyaan)nekochan@example.org, nekochan(nyaan)cat@example.org or
224
241
  # nekochan(nyaan)@example.org
225
- v['address'] = cv[1] + cv[3]
226
- v['comment'] = cv[2]
242
+ v[:address] = cv[1] + cv[3]
243
+ v[:comment] = cv[2]
227
244
  end
228
- v['name'] = ''
229
245
  readbuffer << v
230
246
  end
231
247
  end
232
248
 
233
249
  readbuffer.each do |e|
234
- unless e['address'] =~ /\A.+[@].+\z/
250
+ # The element must not include any character except from 0x20 to 0x7e.
251
+ next if e[:address] =~ /[^\x20-\x7e]/
252
+
253
+ unless e[:address] =~ /\A.+[@].+\z/
235
254
  # Allow if the argument is MAILER-DAEMON
236
- next unless Sisimai::RFC5322.is_mailerdaemon(e['address'])
255
+ next unless Sisimai::RFC5322.is_mailerdaemon(e[:address])
237
256
  end
238
257
 
239
- e['address'] = e['address'].sub(/\A[\[<{('`]/, '')
240
- e['address'] = e['address'].sub(/['`>})\]]\z/, '')
258
+ # Remove angle brackets, other brackets, and quotations: []<>{}'`
259
+ # except a domain part is an IP address like neko@[192.0.2.222]
260
+ e[:address] = e[:address].sub(/\A[\[<{('`]/, '')
261
+ e[:address] = e[:address].sub(/['`>})]\z/, '')
262
+ e[:address] = e[:address].sub(/\]\z/, '') unless e[:address] =~ /[@]\[[0-9A-Z:\.]+\]\z/i
241
263
 
242
- unless e['address'] =~ /\A["].+["][@]/
264
+ unless e[:address] =~ /\A["].+["][@]/
243
265
  # Remove double-quotations
244
- e['address'] = e['address'].sub(/\A["]/, '')
245
- e['address'] = e['address'].sub(/["]\z/, '')
266
+ e[:address] = e[:address].sub(/\A["]/, '')
267
+ e[:address] = e[:address].sub(/["]\z/, '')
246
268
  end
247
269
 
248
270
  if addrs
249
271
  # Almost compatible with parse() method, returns email address only
250
- e.delete('name')
251
- e.delete('comment')
272
+ e.delete(:name)
273
+ e.delete(:comment)
252
274
  else
253
275
  # Remove double-quotations, trailing spaces.
254
- %w|name comment|.each do |f|
276
+ [:name, :comment].each do |f|
255
277
  e[f] = e[f].sub(/\A\s*/, '')
256
278
  e[f] = e[f].sub(/\s*\z/, '')
257
279
  end
258
- e['name'] = e['name'].sub(/\A["]/, '')
259
- e['name'] = e['name'].sub(/["]\z/, '')
280
+ e[:comment] = '' unless e[:comment] =~ /\A[(].+[)]/
281
+
282
+ e[:name] = e[:name].squeeze(' ') unless e[:name] =~ /\A["].+["]\z/
283
+ e[:name] = e[:name].sub(/\A["]/, '') unless e[:name] =~ /\A["].+["][@]/
284
+ e[:name] = e[:name].sub(/["]\z/, '')
260
285
  end
261
286
  addrtables << e
262
287
  end
@@ -269,93 +294,43 @@ module Sisimai
269
294
  # @param [Array] argvs List of strings including email address
270
295
  # @return [Array, Nil] Email address list or Undef when there is no
271
296
  # email address in the argument
272
- # @example Parse email address
273
- # parse( [ 'Neko <neko@example.cat>' ] ) #=> [ 'neko@example.cat' ]
274
- def self.parse(argvs)
297
+ # @until v4.22.1
298
+ def self.parse(argvs = nil)
299
+ return nil unless argvs
300
+ return nil unless argvs.is_a? Array
301
+ return nil if argvs.empty?
302
+
303
+ warn ' ***warning: Sisimai::Address.parse is marked as obsoleted'
275
304
  addrs = []
305
+
276
306
  argvs.each do |e|
277
307
  # Parse each element in the array
278
308
  # 1. The element must include '@'.
279
309
  # 2. The element must not include character except from 0x20 to 0x7e.
280
310
  next unless e
281
- unless e =~ /[@]/
282
- # Allow if the argument is MAILER-DAEMON
283
- next unless Sisimai::RFC5322.is_mailerdaemon(e)
284
- end
285
- next if e =~ /[^\x20-\x7e]/
311
+ next unless e.is_a? Object::String
312
+ next if e.empty?
286
313
 
287
- v = Sisimai::Address.s3s4(e)
288
- if v.size > 0
289
- # The element includes a valid email address
290
- addrs << v
291
- end
314
+ v = Sisimai::Address.find(e, 1) || []
315
+ next if v.empty?
316
+ v.each { |f| addrs << f[:address] }
292
317
  end
293
318
 
294
- return nil unless addrs.size > 0
319
+ return nil if addrs.empty?
295
320
  return addrs
296
321
  end
297
322
 
298
323
  # Runs like ruleset 3,4 of sendmail.cf
299
- # @param [String] email Text including an email address
324
+ # @param [String] input Text including an email address
300
325
  # @return [String] Email address without comment, brackets
301
- # @example Parse email address
302
- # s3s4( '<neko@example.cat>' ) #=> 'neko@example.cat'
326
+ # @example s3s4('<neko@example.cat>') #=> 'neko@example.cat'
303
327
  def self.s3s4(input)
304
- unless input =~ /[ ]/
305
- # no space character between " and < .
306
- # no space character between " and < .
307
- input = input.sub(/\A(.+)"<(.+)\z/, '\1" <\2') # "=?ISO-2022-JP?B?....?="<user@example.org>,
308
- input = input.sub(/\A(.+)[?]=<(.+)\z/, '\1?= <\2') # =?ISO-2022-JP?B?....?=<user@example.org>
309
-
310
- # comment-part<localpart@domainpart>
311
- input = input.sub(/[<]/, ' <') unless input =~ /\A[<]/
312
- input = input.sub(/[>]/, '> ') unless input =~ /[>]\z/
313
- end
314
-
315
- canon = ''
316
- addrs = []
317
- token = input.split(' ')
318
-
319
- token.map! do |e|
320
- # Convert character entity; "&lt;" -> ">", "&gt;" -> "<".
321
- e = e.gsub(/&lt;/, '<')
322
- e = e.gsub(/&gt;/, '>')
323
- e = e.gsub(/,\z/, '')
324
- end
325
-
326
- if token.size == 1
327
- addrs << token[0]
328
-
329
- else
330
- token.each do |e|
331
- e.chomp
332
- unless e =~ /\A[<]?.+[@][-.0-9A-Za-z]+[.]?[A-Za-z]{2,}[>]?\z/
333
- # Check whether the element is mailer-daemon or not
334
- next unless Sisimai::RFC5322.is_mailerdaemon(e)
335
- end
336
- addrs << e
337
- end
338
- end
339
-
340
- if addrs.size > 1
341
- # Get the first element which is <...> format string from @addrs array.
342
- canon = addrs.detect { |e| e =~ /\A[<].+[>]\z/ } || ''
343
- canon = addrs[0] if canon.size < 1
344
-
345
- else
346
- canon = addrs.shift
347
- end
328
+ return nil unless input
329
+ return input unless input.is_a? Object::String
348
330
 
349
- return '' if !canon || canon == ''
350
- canon = canon.delete('<>[]():;') # Remove brackets, colons
351
-
352
- if canon =~ /\A["].+["][@].+\z/
353
- canon = canon.delete(%q|{}'`|) # "localpart..."@example.org
354
- else
355
- canon = canon.delete(%q|{}'"`|) # Remove brackets, quotations
356
- end
357
-
358
- return canon
331
+ addrs = Sisimai::Address.find(input, 1) || []
332
+ return input if addrs.empty?
333
+ return addrs[0][:address]
359
334
  end
360
335
 
361
336
  # Expand VERP: Get the original recipient address from VERP
@@ -368,9 +343,8 @@ module Sisimai
368
343
  verp0 = ''
369
344
 
370
345
  if cv = local.match(/\A[-_\w]+?[+](\w[-._\w]+\w)[=](\w[-.\w]+\w)\z/)
371
- verp0 = cv[1] + '@' + cv[2]
346
+ verp0 = sprintf("%s@%s", cv[1], cv[2])
372
347
  return verp0 if Sisimai::RFC5322.is_emailaddress(verp0)
373
-
374
348
  else
375
349
  return ''
376
350
  end
@@ -399,74 +373,68 @@ module Sisimai
399
373
  :verp, # [String] VERP
400
374
  :alias, # [String] alias of the email address
401
375
  ]
376
+ @@rwaccessors = [
377
+ :name, # [String] Display name
378
+ :comment, # [String] Comment
379
+ ]
402
380
  @@roaccessors.each { |e| attr_reader e }
381
+ @@rwaccessors.each { |e| attr_accessor e }
403
382
 
404
383
  # Constructor of Sisimai::Address
405
- # @param <str> [String] email Email address
384
+ # @param <str> [String] argv1 Email address
406
385
  # @return [Sisimai::Address, Nil] Object or Undef when the email
407
386
  # address was not valid
408
- def initialize(email)
409
- return nil unless email
387
+ def initialize(argv1)
388
+ return nil unless argv1
389
+
390
+ addrs = Sisimai::Address.find(argv1)
391
+ return nil unless addrs
392
+ return nil if addrs.empty?
393
+ thing = addrs.shift
410
394
 
411
- if cv = email.match(/\A([^@]+)[@]([^@]+)\z/)
395
+ if cv = thing[:address].match(/\A([^\s]+)[@]([^@]+)\z/) ||
396
+ cv = thing[:address].match(/\A(["].+?["])[@]([^@]+)\z/)
412
397
  # Get the local part and the domain part from the email address
413
398
  lpart = cv[1]
414
399
  dpart = cv[2]
400
+ email = Sisimai::Address.expand_verp(thing[:address])
401
+ aname = nil
415
402
 
416
- # Remove MIME-Encoded comment part
417
- lpart = lpart.sub(/\A=[?].+[?]b[?].+[?]=/, '')
418
- lpart = lpart.delete(%q|`'"<>|) unless lpart =~ /\A["].+["]\z/
419
- aflag = false
420
- addr0 = sprintf('%s@%s', lpart, dpart)
421
- addr1 = Sisimai::Address.expand_verp(addr0)
422
-
423
- if addr1.size < 1
424
- addr1 = Sisimai::Address.expand_alias(addr0)
425
- aflag = true if addr1.size > 0
403
+ if email.empty?
404
+ # Is not VERP address, try to expand the address as an alias
405
+ email = Sisimai::Address.expand_alias(thing[:address])
406
+ aname = true unless email.empty?
426
407
  end
427
408
 
428
- if addr1.size > 0
429
- # The email address is VERP or alias
430
- addrs = addr1.split('@')
431
- if aflag
432
- # The email address is an alias
433
- @alias = addr0
434
-
409
+ if email =~ /\A.+[@].+?\z/
410
+ # The address is a VERP or an alias
411
+ if aname
412
+ # The address is an alias: neko+nyaan@example.jp
413
+ @alias = thing[:address]
435
414
  else
436
- # The email address is a VERP
437
- @verp = addr0
415
+ # The address is a VERP: b+neko=example.jp@example.org
416
+ @verp = thing[:address]
438
417
  end
439
- @user = addrs[0]
440
- @host = addrs[1]
441
-
442
- else
443
- # The email address is neither VERP nor alias.
444
- @user = lpart
445
- @host = dpart
446
-
447
418
  end
448
- @address = sprintf('%s@%s', @user, @host)
449
- @alias ||= ''
450
- @verp ||= ''
419
+ @user = lpart
420
+ @host = dpart
421
+ @address = sprintf("%s@%s", lpart, dpart)
451
422
 
452
423
  else
453
424
  # The argument does not include "@"
454
- return nil unless Sisimai::RFC5322.is_mailerdaemon(email)
455
- @alias ||= ''
456
- @verp ||= ''
457
- @host ||= ''
425
+ return nil unless Sisimai::RFC5322.is_mailerdaemon(thing[:address])
426
+ return nil if thing[:address] =~ /[ ]/
458
427
 
459
- if cv = email.match(/[<]([^ ]+)[>]/)
460
- # Mail Delivery Subsystem <MAILER-DAEMON>
461
- @user = cv[1]
462
- @address = cv[1]
463
- else
464
- return nil if email =~ /[ ]/
465
- # The argument does not include " "
466
- @user = email
467
- @address = email
468
- end
428
+ # The argument does not include " "
429
+ @user = thing[:address]
430
+ @host ||= ''
431
+ @address = thing[:address]
469
432
  end
433
+
434
+ @alias ||= ''
435
+ @verp ||= ''
436
+ @name = thing[:name] || ''
437
+ @comment = thing[:comment] || ''
470
438
  end
471
439
 
472
440
  # Check whether the object has valid content or not
@@ -479,7 +447,7 @@ module Sisimai
479
447
  # Returns the value of address as String
480
448
  # @return [String] Email address
481
449
  def to_json(*)
482
- return to_s
450
+ return self.address.to_s
483
451
  end
484
452
 
485
453
  # Returns the value of address as String
data/lib/sisimai/data.rb CHANGED
@@ -61,30 +61,21 @@ module Sisimai
61
61
  thing = {}
62
62
 
63
63
  # Create email address object
64
- x0 = Sisimai::Address.find(argvs['addresser'])
65
- y0 = Sisimai::Address.find(argvs['recipient'])
66
-
67
- return nil unless x0.is_a? Array
68
- return nil unless y0.is_a? Array
69
-
70
- thing[:addresser] = Sisimai::Address.new(x0[0]['address'])
71
- return nil unless thing[:addresser].is_a? Sisimai::Address
72
- return nil if thing[:addresser].void
73
- thing[:senderdomain] = thing[:addresser].host
74
-
75
- thing[:recipient] = Sisimai::Address.new(y0[0]['address'])
76
- return nil unless thing[:recipient].is_a? Sisimai::Address
77
- return nil if thing[:recipient].void
78
- thing[:destination] = thing[:recipient].host
79
- thing[:alias] = argvs['alias'] || ''
80
-
81
- @addresser = thing[:addresser]
82
- @senderdomain = thing[:senderdomain]
83
- @recipient = thing[:recipient]
84
- @destination = thing[:destination]
85
- @alias = thing[:alias]
86
-
87
- @token = Sisimai::String.token(@addresser.address, @recipient.address, argvs['timestamp'])
64
+ as = Sisimai::Address.make(argvs['addresser'])
65
+ ar = Sisimai::Address.make({ address: argvs['recipient'] })
66
+
67
+ return nil unless as.is_a? Sisimai::Address
68
+ return nil unless ar.is_a? Sisimai::Address
69
+
70
+ return nil if as.void
71
+ return nil if ar.void
72
+
73
+ @addresser = as
74
+ @recipient = ar
75
+ @senderdomain = as.host
76
+ @destination = ar.host
77
+ @alias = argvs['alias'] || ''
78
+ @token = Sisimai::String.token(as.address, ar.address, argvs['timestamp'])
88
79
  @timestamp = Sisimai::Time.parse(::Time.at(argvs['timestamp']).to_s)
89
80
  @timezoneoffset = argvs['timezoneoffset'] || '+0000'
90
81
  @lhost = argvs['lhost'] || ''
@@ -180,17 +171,20 @@ module Sisimai
180
171
  # Check each header in message/rfc822 part
181
172
  h = f.downcase
182
173
  next unless rfc822data.key?(h)
183
- next unless rfc822data[h].size > 0
184
- next unless Sisimai::RFC5322.is_emailaddress(rfc822data[h])
185
- p['addresser'] = rfc822data[h]
174
+ next if rfc822data[h].empty?
175
+
176
+ j = Sisimai::Address.find(rfc822data[h]) || []
177
+ next if j.empty?
178
+ p['addresser'] = j[0]
186
179
  break
187
180
  end
188
181
 
189
- # Fallback: Get the sender address from the header of the bounced
190
- # email if the address is not set at loop above.
191
- p['addresser'] ||= ''
192
- p['addresser'] = messageobj.header['to'] if p['addresser'].empty?
193
-
182
+ unless p['addresser']
183
+ # Fallback: Get the sender address from the header of the bounced
184
+ # email if the address is not set at loop above.
185
+ j = Sisimai::Address.find(messageobj.header['to']) || []
186
+ p['addresser'] = j[0] unless j.empty?
187
+ end
194
188
  next unless p['addresser']
195
189
  next unless p['recipient']
196
190
 
@@ -302,7 +302,9 @@ module Sisimai
302
302
  def self.takeapart(heads)
303
303
  return {} unless heads
304
304
 
305
- # Convert from string to hash reference
305
+ # 1. Scrub to avoid "invalid byte sequence in UTF-8" exception (#82)
306
+ # 2. Convert from string to hash reference
307
+ heads = heads.scrub('?')
306
308
  heads = heads.gsub(/^[>]+[ ]/m, '')
307
309
 
308
310
  takenapart = {}
@@ -95,7 +95,7 @@ module Sisimai
95
95
  end
96
96
  end
97
97
 
98
- if readcursor & Indicators[:'message-rfc822'] > 0
98
+ if readcursor & Indicators[:'message-rfc822'] == 0
99
99
  # Beginning of the original message part
100
100
  if e =~ Re1[:rfc822]
101
101
  readcursor |= Indicators[:'message-rfc822']
@@ -403,6 +403,7 @@ module Sisimai
403
403
  # May be an email address
404
404
  x = b['recipient'] || ''
405
405
  y = Sisimai::Address.s3s4(cv[1])
406
+ next unless Sisimai::RFC5322.is_emailaddress(y)
406
407
 
407
408
  if x.size > 0 && x != y
408
409
  # There are multiple recipient addresses in the message body.
@@ -424,6 +425,26 @@ module Sisimai
424
425
  break
425
426
  end
426
427
 
428
+ if recipients.zero?
429
+ # Try to get a recipient address from email headers
430
+ rfc822list.each do |e|
431
+ # Check To: header in the original message
432
+ if cv = e.match(/\ATo:\s*(.+)\z/)
433
+ r = Sisimai::Address.find(cv[1], true)
434
+ b = nil
435
+ next if r.empty?
436
+
437
+ if dscontents.size == recipients
438
+ dscontents << Sisimai::Bite::Email.DELIVERYSTATUS
439
+ end
440
+ b = dscontents[-1]
441
+ b['recipient'] = r[0][:address]
442
+ b['agent'] = Sisimai::RFC3464.smtpagent + '::Fallback'
443
+ recipients += 1
444
+ end
445
+ end
446
+ end
447
+
427
448
  return nil unless recipients > 0
428
449
  require 'sisimai/string'
429
450
  require 'sisimai/smtp/status'
@@ -38,9 +38,9 @@ module Sisimai
38
38
  local_part = %r/(?:#{dot_atom}|#{quoted_string})/o
39
39
  domain = %r/(?:#{dot_atom}|#{domain_literal})/o
40
40
 
41
- re[:rfc5322] = %r/#{local_part}[@]#{domain}/o
42
- re[:ignored] = %r/#{local_part}[.]*[@]#{domain}/o
43
- re[:domain] = %r/#{domain}/o
41
+ re[:rfc5322] = %r/\A#{local_part}[@]#{domain}\z/o
42
+ re[:ignored] = %r/\A#{local_part}[.]*[@]#{domain}\z/o
43
+ re[:domain] = %r/\A#{domain}\z/o
44
44
 
45
45
  return re
46
46
  end
@@ -83,6 +83,7 @@ module Sisimai
83
83
  def is_emailaddress(email)
84
84
  return false unless email.is_a?(::String)
85
85
  return false if email =~ %r/(?:[\x00-\x1f]|\x1f)/
86
+ return false if email.size > 254
86
87
  return true if email =~ Re[:ignored]
87
88
  return false
88
89
  end
@@ -116,6 +116,12 @@ module Sisimai
116
116
  :regexp => %r/System incorrectly configured/,
117
117
  },
118
118
  ],
119
+ %r/\A5[.]4[.]1\z/ => [
120
+ {
121
+ :reason => 'userunknown',
122
+ :regexp => %r/Recipient address rejected: Access denied/,
123
+ },
124
+ ],
119
125
  %r/\A5[.]4[.][46]\z/ => [
120
126
  {
121
127
  :reason => 'networkerror',
@@ -234,23 +240,22 @@ module Sisimai
234
240
  ],
235
241
  }
236
242
 
237
- # Detect bounce reason from Google Apps
243
+ # Detect bounce reason from Exchange Online
238
244
  # @param [Sisimai::Data] argvs Parsed email object
239
245
  # @return [String] The bounce reason for Exchange Online
240
- # @see https://support.google.com/a/answer/3726730?hl=en
241
246
  def get(argvs)
242
247
  return nil unless argvs
243
248
  return nil unless argvs.is_a? Sisimai::Data
244
249
  return argvs.reason if argvs.reason.size > 0
245
250
 
246
- statuscode = argvs.deliverystatus.sub(/\A\d[.](\d+[.]\d+)\z/, 'X.\1')
251
+ statuscode = argvs.deliverystatus
247
252
  statusmesg = argvs.diagnosticcode
248
253
  reasontext = ''
249
254
 
250
255
  CodeTable.each_key do |e|
251
256
  # Try to match with each regular expression of delivery status codes
252
257
  next unless statuscode =~ e
253
- Codetable[e].each do |f|
258
+ CodeTable[e].each do |f|
254
259
  # Try to match with each regular expression of error messages
255
260
  next unless statusmesg =~ f[:regexp]
256
261
  reasontext = f[:reason]
@@ -1,4 +1,4 @@
1
1
  # Define the version number of Sisimai
2
2
  module Sisimai
3
- VERSION = '4.22.0'.freeze
3
+ VERSION = '4.22.1'.freeze
4
4
  end
@@ -0,0 +1,81 @@
1
+ Return-Path: <>
2
+ Received: from [redacted].net.br ([redacted].net.br [[redacted]])
3
+ by inbound-smtp.us-east-1.amazonaws.com with SMTP id [redacted]
4
+ for bounces+OkGew2p6d9r5RJ5x9g1nxNq73zQDL1Zo-[redacted].br@example.net.br;
5
+ Sat, 12 Aug 2017 21:57:09 +0000 (UTC)
6
+ X-SES-Spam-Verdict: PASS
7
+ X-SES-Virus-Verdict: PASS
8
+ Received-SPF: pass (spfCheck: domain of [redacted].net.br designates [redacted] as permitted sender) client-ip=[redacted]; envelope-from=postmaster@example.net.br; helo=[redacted].net.br;
9
+ Authentication-Results: amazonses.com;
10
+ spf=pass (spfCheck: domain of [redacted].net.br designates [redacted] as permitted sender) client-ip=[redacted]; envelope-from=postmaster@example.net.br; helo=[redacted].net.br;
11
+ X-SES-RECEIPT: [redacted]
12
+ X-SES-DKIM-SIGNATURE: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/simple;
13
+ s=224i4yxa5dv7c2xz3womw6peuasteono; d=amazonses.com; t=1502575030;
14
+ h=X-SES-RECEIPT:Date:From:Subject:To:MIME-Version:Content-Type:Message-Id;
15
+ bh=BWidKjC4S0NbHfJyNpzlk805DzVa5/bMRs6QoN8OTpo=;
16
+ b=kOtH6OmyLBgqsUExvRp4/yNJTZheGi08a5EMplRGuRxHqoziBHHQjOh0UwqDMIKZ
17
+ IFzbiAKX4lRuWFi/Ntb8/sijgcPyKqBu269VwAnGmUYEJvAV55D4uXnJjftu+CT7WCr
18
+ L6SkHjYmTQSWpTwvXmM1YDcUP2dkpWV5+Ko9S4lY=
19
+ Received: by [redacted].net.br (Postfix)
20
+ id 84FFABD9; Sat, 12 Aug 2017 21:57:09 +0000 (UTC)
21
+ Date: Sat, 12 Aug 2017 21:57:09 +0000 (UTC)
22
+ From: MAILER-DAEMON@example.net.br (Mail Delivery System)
23
+ Subject: Undelivered Mail Returned to Sender
24
+ To: bounces+OkGew2p6d9r5RJ5x9g1nxNq73zQDL1Zo-[redacted].br@example.net.br
25
+ Auto-Submitted: auto-replied
26
+ MIME-Version: 1.0
27
+ Content-Type: multipart/report; report-type=delivery-status;
28
+ boundary="F180CB7A.1502575029/[redacted].net.br"
29
+ Content-Transfer-Encoding: 8bit
30
+ Message-Id: <20170812215709.84FFABD9@example.net.br>
31
+
32
+ This is a MIME-encapsulated message.
33
+
34
+ --F180CB7A.1502575029/[redacted].net.br
35
+ Content-Description: Notification
36
+ Content-Type: text/plain; charset=utf-8
37
+ Content-Transfer-Encoding: 8bit
38
+
39
+ This is the mail system at host [redacted].net.br.
40
+
41
+ I'm sorry to have to inform you that your message could not
42
+ be delivered to one or more recipients. It's attached below.
43
+
44
+ For further assistance, please send mail to postmaster.
45
+
46
+ If you do so, please include this problem report. You can
47
+ delete your own text from the attached returned message.
48
+
49
+ The mail system
50
+
51
+ <kijitora@example.br>: host
52
+ [redacted]-br.mail.protection.outlook.com[207.46.163.74] said: 550 5.4.1
53
+ [kijitora@example.br]: Recipient address rejected: Access denied
54
+ [BL2NAM02FT061.eop-nam02.prod.protection.outlook.com] (in reply to RCPT TO
55
+ command)
56
+
57
+ --F180CB7A.1502575029/[redacted].net.br
58
+ Content-Description: Delivery report
59
+ Content-Type: message/delivery-status
60
+ Content-Transfer-Encoding: 8bit
61
+
62
+ Reporting-MTA: dns; [redacted].net.br
63
+ X-Postfix-Queue-ID: F180CB7A
64
+ X-Postfix-Sender: rfc822; bounces+OkGew2p6d9r5RJ5x9g1nxNq73zQDL1Zo-[redacted].br@example.net.br
65
+ Arrival-Date: Sat, 12 Aug 2017 21:56:53 +0000 (UTC)
66
+
67
+ Final-Recipient: rfc822; kijitora@example.br
68
+ Original-Recipient: rfc822;kijitora@example.br
69
+ Action: failed
70
+ Status: 5.4.1
71
+ Remote-MTA: dns; [redacted]-br.mail.protection.outlook.com
72
+ Diagnostic-Code: smtp; 550 5.4.1 [kijitora@example.br]: Recipient
73
+ address rejected: Access denied
74
+ [BL2NAM02FT061.eop-nam02.prod.protection.outlook.com]
75
+
76
+ --F180CB7A.1502575029/[redacted].net.br
77
+ Content-Description: Undelivered Message
78
+ Content-Type: message/rfc822
79
+ Content-Transfer-Encoding: 8bit
80
+
81
+ [from here, just a copy of the original message, with full headers.]
data/sisimai.gemspec CHANGED
@@ -24,5 +24,5 @@ Gem::Specification.new do |spec|
24
24
  spec.add_development_dependency 'bundler', '~> 1.8'
25
25
  spec.add_development_dependency 'rake', '~> 10.0'
26
26
  spec.add_development_dependency 'rspec', '~> 0'
27
- spec.add_runtime_dependency 'oj', '~> 2.14', '>= 2.14.4'
27
+ spec.add_runtime_dependency 'oj', '~> 3.0.0', '>= 3.0.0'
28
28
  end
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: 4.22.0
4
+ version: 4.22.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - azumakuniyuki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-08-22 00:00:00.000000000 Z
11
+ date: 2017-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -58,20 +58,20 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '2.14'
61
+ version: 3.0.0
62
62
  - - ">="
63
63
  - !ruby/object:Gem::Version
64
- version: 2.14.4
64
+ version: 3.0.0
65
65
  type: :runtime
66
66
  prerelease: false
67
67
  version_requirements: !ruby/object:Gem::Requirement
68
68
  requirements:
69
69
  - - "~>"
70
70
  - !ruby/object:Gem::Version
71
- version: '2.14'
71
+ version: 3.0.0
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 2.14.4
74
+ version: 3.0.0
75
75
  description: Sisimai is a Ruby library for analyzing RFC5322 bounce emails and generating
76
76
  structured data from parsed results.
77
77
  email:
@@ -453,6 +453,7 @@ files:
453
453
  - set-of-emails/maildir/bsd/email-postfix-27.eml
454
454
  - set-of-emails/maildir/bsd/email-postfix-28.eml
455
455
  - set-of-emails/maildir/bsd/email-postfix-29.eml
456
+ - set-of-emails/maildir/bsd/email-postfix-30.eml
456
457
  - set-of-emails/maildir/bsd/email-qmail-01.eml
457
458
  - set-of-emails/maildir/bsd/email-qmail-02.eml
458
459
  - set-of-emails/maildir/bsd/email-qmail-03.eml