sisimai 4.22.0-java → 4.22.1-java
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 +4 -4
- data/.travis.yml +1 -1
- data/ChangeLog.md +20 -0
- data/README-JA.md +2 -2
- data/README.md +2 -2
- data/lib/sisimai/address.rb +164 -196
- data/lib/sisimai/data.rb +26 -32
- data/lib/sisimai/message/email.rb +3 -1
- data/lib/sisimai/rfc3464.rb +22 -1
- data/lib/sisimai/rfc5322.rb +4 -3
- data/lib/sisimai/rhost/exchangeonline.rb +9 -4
- data/lib/sisimai/version.rb +1 -1
- data/set-of-emails/maildir/bsd/email-postfix-30.eml +81 -0
- data/sisimai.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 40eed5850997a4c584d55341da4e7a05641f9b98
|
4
|
+
data.tar.gz: f557bc1c7228feb643f7ec9f634051a7741329e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67a63e9284499789d360ea48e1129e22f86af434ab9d0f7f9bcd7f2daac85c42188a19b596097122fbf2377c5583ac530a097c24985a8897e622ab6b9408f813
|
7
|
+
data.tar.gz: 003a233ee04a142f7692b4b8c41064c2ad91d487610039b36f207508cf1da062bec73437e2aeec69af3c6168b6b8b5d6cdd730ac7a129f42d24216aa4460bbf5
|
data/.travis.yml
CHANGED
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:ソースコードの行数 |
|
243
|
-
| テスト件数(spec/,t/,xt/ディレクトリ) |
|
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 |
|
247
|
-
| The number of tests(spec/,t/,xt/) directory |
|
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
|
|
data/lib/sisimai/address.rb
CHANGED
@@ -16,75 +16,102 @@ module Sisimai
|
|
16
16
|
return sprintf('undisclosed-%s-in-headers@libsisimai.org.invalid', local)
|
17
17
|
end
|
18
18
|
|
19
|
-
|
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]
|
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
|
-
#
|
28
|
-
#
|
29
|
-
return nil unless
|
30
|
-
|
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 = {
|
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
|
-
|
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[
|
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[
|
85
|
+
v[:comment] += e
|
59
86
|
|
60
87
|
elsif readcursor & indicators[:'quoted-string'] > 0
|
61
88
|
# "Neko, Nyaan"
|
62
|
-
v[
|
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 = {
|
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[
|
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[
|
81
|
-
p.size > 0 ? (v[p] += e) : (v[
|
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[
|
87
|
-
p =
|
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[
|
125
|
+
v[:address] += e
|
99
126
|
p = ''
|
100
127
|
else
|
101
128
|
# a comment block or a display name
|
102
|
-
p.size > 0 ? (v[
|
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[
|
138
|
+
if v[:address] =~ /["]/
|
112
139
|
# Quoted local part: <"neko(nyaan)"@example.org>
|
113
|
-
v[
|
140
|
+
v[:address] += e
|
114
141
|
|
115
142
|
else
|
116
143
|
# Comment: <neko(nyaan)@example.org>
|
117
144
|
readcursor |= indicators[:'comment-block']
|
118
|
-
v[
|
119
|
-
|
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[
|
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[
|
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[
|
133
|
-
|
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[
|
172
|
+
if v[:address] =~ /["]/
|
143
173
|
# Quoted string in the local part: <"neko(nyaan)"@example.org>
|
144
|
-
v[
|
174
|
+
v[:address] += e
|
145
175
|
|
146
176
|
else
|
147
177
|
# Comment: <neko(nyaan)@example.org>
|
148
178
|
readcursor &= ~indicators[:'comment-block']
|
149
|
-
v[
|
150
|
-
p =
|
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[
|
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[
|
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[
|
204
|
+
v[:name] += e
|
175
205
|
if readcursor & indicators[:'quoted-string'] > 0
|
176
206
|
# "Neko, Nyaan"
|
177
|
-
unless v[
|
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[
|
218
|
+
p.size > 0 ? (v[p] += e) : (v[:name] += e)
|
196
219
|
next
|
197
220
|
end
|
198
221
|
end
|
199
222
|
|
200
|
-
if v[
|
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[
|
228
|
+
if cv = v[:name].match(validemail)
|
206
229
|
# String like an email address will be set to the value of "address"
|
207
|
-
|
208
|
-
# "neko nyaan"@example.org
|
209
|
-
v['address'] = cv[1]
|
230
|
+
v[:address] = sprintf("%s@%s", cv[1], cv[2])
|
210
231
|
|
211
|
-
|
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[
|
234
|
+
v[:address] = v[:name]
|
218
235
|
end
|
219
236
|
|
220
|
-
if v[
|
221
|
-
# Remove the
|
222
|
-
if cv = v[
|
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[
|
226
|
-
v[
|
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
|
-
|
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[
|
255
|
+
next unless Sisimai::RFC5322.is_mailerdaemon(e[:address])
|
237
256
|
end
|
238
257
|
|
239
|
-
|
240
|
-
|
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[
|
264
|
+
unless e[:address] =~ /\A["].+["][@]/
|
243
265
|
# Remove double-quotations
|
244
|
-
e[
|
245
|
-
e[
|
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(
|
251
|
-
e.delete(
|
272
|
+
e.delete(:name)
|
273
|
+
e.delete(:comment)
|
252
274
|
else
|
253
275
|
# Remove double-quotations, trailing spaces.
|
254
|
-
|
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[
|
259
|
-
|
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
|
-
# @
|
273
|
-
|
274
|
-
|
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
|
-
|
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.
|
288
|
-
if v.
|
289
|
-
|
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
|
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]
|
324
|
+
# @param [String] input Text including an email address
|
300
325
|
# @return [String] Email address without comment, brackets
|
301
|
-
# @example
|
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
|
-
|
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; "<" -> ">", ">" -> "<".
|
321
|
-
e = e.gsub(/</, '<')
|
322
|
-
e = e.gsub(/>/, '>')
|
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
|
-
|
350
|
-
|
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]
|
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]
|
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(
|
409
|
-
return nil unless
|
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 =
|
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
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
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
|
429
|
-
# The
|
430
|
-
|
431
|
-
|
432
|
-
|
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
|
437
|
-
@verp
|
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
|
-
@
|
449
|
-
@
|
450
|
-
@
|
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(
|
455
|
-
|
456
|
-
@verp ||= ''
|
457
|
-
@host ||= ''
|
425
|
+
return nil unless Sisimai::RFC5322.is_mailerdaemon(thing[:address])
|
426
|
+
return nil if thing[:address] =~ /[ ]/
|
458
427
|
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
return nil unless
|
68
|
-
return nil unless
|
69
|
-
|
70
|
-
|
71
|
-
return nil
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
184
|
-
|
185
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
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
|
-
#
|
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 = {}
|
data/lib/sisimai/rfc3464.rb
CHANGED
@@ -95,7 +95,7 @@ module Sisimai
|
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
98
|
-
if readcursor & Indicators[:'message-rfc822']
|
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'
|
data/lib/sisimai/rfc5322.rb
CHANGED
@@ -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
|
42
|
-
re[:ignored] = %r
|
43
|
-
re[:domain] = %r
|
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
|
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
|
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
|
-
|
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]
|
data/lib/sisimai/version.rb
CHANGED
@@ -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', '~>
|
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.
|
4
|
+
version: 4.22.1
|
5
5
|
platform: java
|
6
6
|
authors:
|
7
7
|
- azumakuniyuki
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-08-
|
11
|
+
date: 2017-08-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -452,6 +452,7 @@ files:
|
|
452
452
|
- set-of-emails/maildir/bsd/email-postfix-27.eml
|
453
453
|
- set-of-emails/maildir/bsd/email-postfix-28.eml
|
454
454
|
- set-of-emails/maildir/bsd/email-postfix-29.eml
|
455
|
+
- set-of-emails/maildir/bsd/email-postfix-30.eml
|
455
456
|
- set-of-emails/maildir/bsd/email-qmail-01.eml
|
456
457
|
- set-of-emails/maildir/bsd/email-qmail-02.eml
|
457
458
|
- set-of-emails/maildir/bsd/email-qmail-03.eml
|