rims 0.2.1
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/ChangeLog +379 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +566 -0
- data/Rakefile +29 -0
- data/bin/rims +11 -0
- data/lib/rims.rb +45 -0
- data/lib/rims/auth.rb +133 -0
- data/lib/rims/cksum_kvs.rb +68 -0
- data/lib/rims/cmd.rb +809 -0
- data/lib/rims/daemon.rb +338 -0
- data/lib/rims/db.rb +793 -0
- data/lib/rims/error.rb +23 -0
- data/lib/rims/gdbm_kvs.rb +76 -0
- data/lib/rims/hash_kvs.rb +66 -0
- data/lib/rims/kvs.rb +101 -0
- data/lib/rims/lock.rb +151 -0
- data/lib/rims/mail_store.rb +663 -0
- data/lib/rims/passwd.rb +251 -0
- data/lib/rims/pool.rb +88 -0
- data/lib/rims/protocol.rb +71 -0
- data/lib/rims/protocol/decoder.rb +1469 -0
- data/lib/rims/protocol/parser.rb +1114 -0
- data/lib/rims/rfc822.rb +456 -0
- data/lib/rims/server.rb +567 -0
- data/lib/rims/test.rb +391 -0
- data/lib/rims/version.rb +11 -0
- data/load_test/Rakefile +93 -0
- data/rims.gemspec +38 -0
- data/test/test_auth.rb +174 -0
- data/test/test_cksum_kvs.rb +121 -0
- data/test/test_config.rb +533 -0
- data/test/test_daemon_status_file.rb +169 -0
- data/test/test_daemon_waitpid.rb +72 -0
- data/test/test_db.rb +602 -0
- data/test/test_db_recovery.rb +732 -0
- data/test/test_error.rb +97 -0
- data/test/test_gdbm_kvs.rb +32 -0
- data/test/test_hash_kvs.rb +116 -0
- data/test/test_lock.rb +161 -0
- data/test/test_mail_store.rb +750 -0
- data/test/test_passwd.rb +203 -0
- data/test/test_protocol.rb +91 -0
- data/test/test_protocol_auth.rb +121 -0
- data/test/test_protocol_decoder.rb +6490 -0
- data/test/test_protocol_fetch.rb +994 -0
- data/test/test_protocol_request.rb +332 -0
- data/test/test_protocol_search.rb +974 -0
- data/test/test_rfc822.rb +696 -0
- metadata +174 -0
data/lib/rims/passwd.rb
ADDED
@@ -0,0 +1,251 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'digest'
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
module RIMS
|
7
|
+
module Password
|
8
|
+
# expected behavior of a typical password source.
|
9
|
+
# 1. a password source plug-in file is loaded by a
|
10
|
+
# configuration file item of <tt>load_libraries</tt>.
|
11
|
+
# (exceptions: RIMS::Password::PlainSource and
|
12
|
+
# RIMS::Password::HashSource are not need to load.)
|
13
|
+
# 2. RIMS::Authentication.add_plug_in is called to enable a
|
14
|
+
# password source on loading its plug-in file.
|
15
|
+
# 3. a password source object is built from a configuration
|
16
|
+
# file by its <tt>build_from_conf</tt> class method.
|
17
|
+
# 4. if any password source's <tt>raw_passwrd?</tt> method
|
18
|
+
# returns a <tt>false</tt> value, RIMS IMAP server disables
|
19
|
+
# challenge-response authentication options (ex. CRAM-MD5).
|
20
|
+
# 5. a logger object is set to a <tt>logger</tt> write
|
21
|
+
# attribute on a password source object. and the password
|
22
|
+
# source's <tt>start</tt> method is called.
|
23
|
+
# 6. a password source object authenticates a user by its
|
24
|
+
# authentication methods those are <tt>fetch_password</tt>
|
25
|
+
# or <tt>compare_password</tt>.
|
26
|
+
# 7. to deliver a message to a user, a password source object
|
27
|
+
# confirms existence of a user by its <tt>user?</tt>
|
28
|
+
# method.
|
29
|
+
# 8. a <tt>stop</tt> method of a password source object is
|
30
|
+
# called at stopping server.
|
31
|
+
class Source
|
32
|
+
attr_writer :logger
|
33
|
+
|
34
|
+
def start
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop
|
38
|
+
end
|
39
|
+
|
40
|
+
# this method declares that this password source returns a plain
|
41
|
+
# text password or do not it.
|
42
|
+
# if this method will return a <tt>true</tt> value,
|
43
|
+
# <tt>fetch_password</tt> may be called.
|
44
|
+
# if this method will return a <tt>false</tt> value,
|
45
|
+
# <tt>fetch_password</tt> will never be called.
|
46
|
+
def raw_password?
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
def user?(username)
|
51
|
+
raise NotImplementedError, 'not implemented.'
|
52
|
+
end
|
53
|
+
|
54
|
+
# if a user exists, this method should return the user's plain
|
55
|
+
# text password.
|
56
|
+
# if a user does not exist, this method should return a
|
57
|
+
# <tt>nil</tt> value.
|
58
|
+
def fetch_password(username)
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
# if a user exists and the user's password is right, this method
|
63
|
+
# should return a true context value (not <tt>false</tt>,
|
64
|
+
# <tt>nil</tt>).
|
65
|
+
# if a user exists and the user's password is wrong, this method
|
66
|
+
# should return a false context value (<tt>false<</tt> or
|
67
|
+
# <tt>nil</tt>).
|
68
|
+
# if a user does not exist, this method should return a false
|
69
|
+
# context value (<tt>false<</tt> or <tt>nil</tt>).
|
70
|
+
def compare_password(username, password)
|
71
|
+
if (raw_password = fetch_password(username)) then
|
72
|
+
password == raw_password
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.build_from_conf(config)
|
77
|
+
raise NotImplementedError, 'not implemented.'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class PlainSource < Source
|
82
|
+
def initialize
|
83
|
+
@passwd = {}
|
84
|
+
end
|
85
|
+
|
86
|
+
def start
|
87
|
+
if (@logger.debug?) then
|
88
|
+
@passwd.each_key do |name|
|
89
|
+
@logger.debug("user name: #{name}")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def stop
|
96
|
+
@passwd.clear
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def raw_password?
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
def entry(username, password)
|
105
|
+
@passwd[username] = password
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def user?(username)
|
110
|
+
@passwd.key? username
|
111
|
+
end
|
112
|
+
|
113
|
+
def fetch_password(username)
|
114
|
+
@passwd[username]
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.build_from_conf(config)
|
118
|
+
plain_src = self.new
|
119
|
+
for user_entry in config
|
120
|
+
plain_src.entry(user_entry['user'], user_entry['pass'])
|
121
|
+
end
|
122
|
+
|
123
|
+
plain_src
|
124
|
+
end
|
125
|
+
end
|
126
|
+
Authentication.add_plug_in('plain', PlainSource)
|
127
|
+
|
128
|
+
class HashSource < Source
|
129
|
+
class Entry
|
130
|
+
def self.encode(digest, stretch_count, salt, password)
|
131
|
+
salt_password = salt.b + password.b
|
132
|
+
digest.update(salt_password)
|
133
|
+
stretch_count.times do
|
134
|
+
digest.update(digest.digest + salt_password)
|
135
|
+
end
|
136
|
+
digest.hexdigest
|
137
|
+
end
|
138
|
+
|
139
|
+
def initialize(digest_factory, stretch_count, salt, hash)
|
140
|
+
@digest_factory = digest_factory
|
141
|
+
@stretch_count = stretch_count
|
142
|
+
@salt = salt
|
143
|
+
@hash = hash
|
144
|
+
end
|
145
|
+
|
146
|
+
def hash_type
|
147
|
+
@digest_factory.to_s.sub(/^Digest::/, '')
|
148
|
+
end
|
149
|
+
|
150
|
+
attr_reader :stretch_count
|
151
|
+
attr_reader :salt
|
152
|
+
attr_reader :hash
|
153
|
+
|
154
|
+
def salt_base64
|
155
|
+
Protocol.encode_base64(@salt)
|
156
|
+
end
|
157
|
+
|
158
|
+
def to_s
|
159
|
+
[ hash_type, @stretch_count, salt_base64, @hash ].join(':')
|
160
|
+
end
|
161
|
+
|
162
|
+
def compare(password)
|
163
|
+
self.class.encode(@digest_factory.new, @stretch_count, @salt, password) == @hash
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.search_digest_factory(hash_type)
|
168
|
+
if (digest_factory = Digest.const_get(hash_type)) then
|
169
|
+
if (digest_factory < Digest::Base) then
|
170
|
+
return digest_factory
|
171
|
+
end
|
172
|
+
end
|
173
|
+
raise TypeError, "not a digest factory: #{hash_type}"
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.make_salt_generator(octets)
|
177
|
+
proc{ SecureRandom.random_bytes(octets) }
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.make_entry(digest_factory, stretch_count, salt, password)
|
181
|
+
hash = Entry.encode(digest_factory.new, stretch_count, salt, password)
|
182
|
+
Entry.new(digest_factory, stretch_count, salt, hash)
|
183
|
+
end
|
184
|
+
|
185
|
+
# hash password format:
|
186
|
+
# [hash type]:[stretch count]:[base64 encoded salt]:[password hash hex digest]
|
187
|
+
# example:
|
188
|
+
# SHA256:1000:2tImt4kLqLM=:756f633bf70613555aa93a5be1e5d93adfe87160e794abc6294c3b58a18f93aa
|
189
|
+
def self.parse_entry(password_hash)
|
190
|
+
hash_type, stretch_count, salt_base64, hash = password_hash.split(':', 4)
|
191
|
+
digest_factory = search_digest_factory(hash_type)
|
192
|
+
stretch_count = stretch_count.to_i
|
193
|
+
salt = Protocol.decode_base64(salt_base64)
|
194
|
+
Entry.new(digest_factory, stretch_count, salt, hash)
|
195
|
+
end
|
196
|
+
|
197
|
+
def initialize
|
198
|
+
@passwd = {}
|
199
|
+
end
|
200
|
+
|
201
|
+
def start
|
202
|
+
if (@logger.debug?) then
|
203
|
+
for name, entry in @passwd
|
204
|
+
@logger.debug("user name: #{name}")
|
205
|
+
@logger.debug("password hash: #{entry}")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
nil
|
209
|
+
end
|
210
|
+
|
211
|
+
def stop
|
212
|
+
@passwd.clear
|
213
|
+
nil
|
214
|
+
end
|
215
|
+
|
216
|
+
def raw_password?
|
217
|
+
false
|
218
|
+
end
|
219
|
+
|
220
|
+
def add(username, entry)
|
221
|
+
@passwd[username] = entry
|
222
|
+
self
|
223
|
+
end
|
224
|
+
|
225
|
+
def user?(username)
|
226
|
+
@passwd.key? username
|
227
|
+
end
|
228
|
+
|
229
|
+
def compare_password(username, password)
|
230
|
+
if (entry = @passwd[username]) then
|
231
|
+
entry.compare(password)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def self.build_from_conf(config)
|
236
|
+
hash_src = self.new
|
237
|
+
for user_entry in config
|
238
|
+
hash_src.add(user_entry['user'], parse_entry(user_entry['hash']))
|
239
|
+
end
|
240
|
+
|
241
|
+
hash_src
|
242
|
+
end
|
243
|
+
end
|
244
|
+
Authentication.add_plug_in('hash', HashSource)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Local Variables:
|
249
|
+
# mode: Ruby
|
250
|
+
# indent-tabs-mode: nil
|
251
|
+
# End:
|
data/lib/rims/pool.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module RIMS
|
4
|
+
class ObjectPool
|
5
|
+
class ObjectHolder
|
6
|
+
def initialize(object_pool, object_key)
|
7
|
+
@object_pool = object_pool
|
8
|
+
@object_key = object_key
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :object_key
|
12
|
+
|
13
|
+
def object_destroy
|
14
|
+
end
|
15
|
+
|
16
|
+
# optional block is called when a mail store is closed.
|
17
|
+
def return_pool(**name_args, &block) # yields:
|
18
|
+
@object_pool.put(self, **name_args, &block)
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class ReferenceCount
|
24
|
+
def initialize(count, object_holder)
|
25
|
+
@count = count
|
26
|
+
@object_holder = object_holder
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :count
|
30
|
+
attr_reader :object_holder
|
31
|
+
|
32
|
+
def object_destroy
|
33
|
+
@object_holder.object_destroy
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(&object_factory) # yields: object_pool, object_key, object_lock
|
38
|
+
@object_factory = object_factory
|
39
|
+
@pool_map = {}
|
40
|
+
@pool_lock = Mutex.new
|
41
|
+
@object_lock_map = Hash.new{|hash, key| hash[key] = ReadWriteLock.new }
|
42
|
+
end
|
43
|
+
|
44
|
+
def empty?
|
45
|
+
@pool_map.empty?
|
46
|
+
end
|
47
|
+
|
48
|
+
# optional block is called when a new object is added to an object pool.
|
49
|
+
def get(object_key, timeout_seconds: ReadWriteLock::DEFAULT_TIMEOUT_SECONDS) # yields:
|
50
|
+
object_lock = @pool_lock.synchronize{ @object_lock_map[object_key] }
|
51
|
+
object_lock.write_synchronize(timeout_seconds) {
|
52
|
+
if (@pool_lock.synchronize{ @pool_map.key? object_key }) then
|
53
|
+
ref_count = @pool_lock.synchronize{ @pool_map[object_key] }
|
54
|
+
else
|
55
|
+
yield if block_given?
|
56
|
+
object_holder = @object_factory.call(self, object_key, object_lock)
|
57
|
+
ref_count = ReferenceCount.new(0, object_holder)
|
58
|
+
@pool_lock.synchronize{ @pool_map[object_key] = ref_count }
|
59
|
+
end
|
60
|
+
ref_count.count >= 0 or raise 'internal error'
|
61
|
+
ref_count.count += 1
|
62
|
+
ref_count.object_holder
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
# optional block is called when an object is deleted from an object pool.
|
67
|
+
def put(object_holder, timeout_seconds: ReadWriteLock::DEFAULT_TIMEOUT_SECONDS) # yields:
|
68
|
+
object_lock = @pool_lock.synchronize{ @object_lock_map[object_holder.object_key] }
|
69
|
+
object_lock.write_synchronize(timeout_seconds) {
|
70
|
+
ref_count = @pool_lock.synchronize{ @pool_map[object_holder.object_key] } or raise 'internal error'
|
71
|
+
ref_count.object_holder.equal? object_holder or raise 'internal error'
|
72
|
+
ref_count.count > 0 or raise 'internal error'
|
73
|
+
ref_count.count -= 1
|
74
|
+
if (ref_count.count == 0) then
|
75
|
+
@pool_lock.synchronize{ @pool_map.delete(object_holder.object_key) }
|
76
|
+
ref_count.object_destroy
|
77
|
+
yield if block_given?
|
78
|
+
end
|
79
|
+
}
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Local Variables:
|
86
|
+
# mode: Ruby
|
87
|
+
# indent-tabs-mode: nil
|
88
|
+
# End:
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module RIMS
|
4
|
+
class ProtocolError < Error
|
5
|
+
end
|
6
|
+
|
7
|
+
class SyntaxError < ProtocolError
|
8
|
+
end
|
9
|
+
|
10
|
+
class MessageSetSyntaxError < SyntaxError
|
11
|
+
end
|
12
|
+
|
13
|
+
module Protocol
|
14
|
+
def quote(s)
|
15
|
+
qs = ''.encode(s.encoding)
|
16
|
+
case (s)
|
17
|
+
when /"/, /\n/
|
18
|
+
qs << '{' << s.bytesize.to_s << "}\r\n" << s
|
19
|
+
else
|
20
|
+
qs << '"' << s << '"'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
module_function :quote
|
24
|
+
|
25
|
+
def compile_wildcard(pattern)
|
26
|
+
src = '\A'
|
27
|
+
src << pattern.gsub(/.*?[*%]/) {|s| Regexp.quote(s[0..-2]) + '.*' }
|
28
|
+
src << Regexp.quote($') if $'
|
29
|
+
src << '\z'
|
30
|
+
Regexp.compile(src)
|
31
|
+
end
|
32
|
+
module_function :compile_wildcard
|
33
|
+
|
34
|
+
def io_data_log(str)
|
35
|
+
s = '<'
|
36
|
+
s << str.encoding.to_s
|
37
|
+
if (str.ascii_only?) then
|
38
|
+
s << ':ascii_only'
|
39
|
+
end
|
40
|
+
s << '> ' << str.inspect
|
41
|
+
end
|
42
|
+
module_function :io_data_log
|
43
|
+
|
44
|
+
def encode_base64(plain_txt)
|
45
|
+
[ plain_txt ].pack('m').each_line.map{|line| line.strip }.join('')
|
46
|
+
end
|
47
|
+
module_function :encode_base64
|
48
|
+
|
49
|
+
def decode_base64(base64_txt)
|
50
|
+
base64_txt.unpack('m')[0]
|
51
|
+
end
|
52
|
+
module_function :decode_base64
|
53
|
+
|
54
|
+
autoload :FetchBody, 'rims/protocol/parser'
|
55
|
+
autoload :RequestReader, 'rims/protocol/parser'
|
56
|
+
autoload :AuthenticationReader, 'rims/protocol/parser'
|
57
|
+
autoload :SearchParser, 'rims/protocol/parser'
|
58
|
+
autoload :FetchParser, 'rims/protocol/parser'
|
59
|
+
autoload :Decoder, 'rims/protocol/decoder'
|
60
|
+
|
61
|
+
def body(symbol: nil, option: nil, section: nil, section_list: nil, partial_origin: nil, partial_size: nil)
|
62
|
+
FetchBody.new(symbol, option, section, section_list, partial_origin, partial_size)
|
63
|
+
end
|
64
|
+
module_function :body
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Local Variables:
|
69
|
+
# mode: Ruby
|
70
|
+
# indent-tabs-mode: nil
|
71
|
+
# End:
|
@@ -0,0 +1,1469 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'net/imap'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
module RIMS
|
8
|
+
module Protocol
|
9
|
+
class Decoder
|
10
|
+
def self.new_decoder(*args, **opts)
|
11
|
+
InitialDecoder.new(*args, **opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.repl(decoder, input, output, logger)
|
15
|
+
response_write = proc{|res|
|
16
|
+
begin
|
17
|
+
last_line = nil
|
18
|
+
for data in res
|
19
|
+
logger.debug("response data: #{Protocol.io_data_log(data)}") if logger.debug?
|
20
|
+
output << data
|
21
|
+
last_line = data
|
22
|
+
end
|
23
|
+
output.flush
|
24
|
+
logger.info("server response: #{last_line.strip}")
|
25
|
+
rescue
|
26
|
+
logger.error('response write error.')
|
27
|
+
logger.error($!)
|
28
|
+
raise
|
29
|
+
end
|
30
|
+
}
|
31
|
+
|
32
|
+
decoder.ok_greeting{|res| response_write.call(res) }
|
33
|
+
|
34
|
+
request_reader = Protocol::RequestReader.new(input, output, logger)
|
35
|
+
loop do
|
36
|
+
begin
|
37
|
+
atom_list = request_reader.read_command
|
38
|
+
rescue
|
39
|
+
logger.error('invalid client command.')
|
40
|
+
logger.error($!)
|
41
|
+
response_write.call([ "* BAD client command syntax error\r\n" ])
|
42
|
+
next
|
43
|
+
end
|
44
|
+
|
45
|
+
break unless atom_list
|
46
|
+
|
47
|
+
tag, command, *opt_args = atom_list
|
48
|
+
logger.info("client command: #{tag} #{command}")
|
49
|
+
logger.debug("client command parameter: #{opt_args.inspect}") if logger.debug?
|
50
|
+
|
51
|
+
begin
|
52
|
+
case (command.upcase)
|
53
|
+
when 'CAPABILITY'
|
54
|
+
decoder.capability(tag, *opt_args) {|res| response_write.call(res) }
|
55
|
+
when 'NOOP'
|
56
|
+
decoder.noop(tag, *opt_args) {|res| response_write.call(res) }
|
57
|
+
when 'LOGOUT'
|
58
|
+
decoder.logout(tag, *opt_args) {|res| response_write.call(res) }
|
59
|
+
when 'AUTHENTICATE'
|
60
|
+
decoder.authenticate(tag, input, output, *opt_args) {|res| response_write.call(res) }
|
61
|
+
when 'LOGIN'
|
62
|
+
decoder.login(tag, *opt_args) {|res| response_write.call(res) }
|
63
|
+
when 'SELECT'
|
64
|
+
decoder.select(tag, *opt_args) {|res| response_write.call(res) }
|
65
|
+
when 'EXAMINE'
|
66
|
+
decoder.examine(tag, *opt_args) {|res| response_write.call(res) }
|
67
|
+
when 'CREATE'
|
68
|
+
decoder.create(tag, *opt_args) {|res| response_write.call(res) }
|
69
|
+
when 'DELETE'
|
70
|
+
decoder.delete(tag, *opt_args) {|res| response_write.call(res) }
|
71
|
+
when 'RENAME'
|
72
|
+
decoder.rename(tag, *opt_args) {|res| response_write.call(res) }
|
73
|
+
when 'SUBSCRIBE'
|
74
|
+
decoder.subscribe(tag, *opt_args) {|res| response_write.call(res) }
|
75
|
+
when 'UNSUBSCRIBE'
|
76
|
+
decoder.unsubscribe(tag, *opt_args) {|res| response_write.call(res) }
|
77
|
+
when 'LIST'
|
78
|
+
decoder.list(tag, *opt_args) {|res| response_write.call(res) }
|
79
|
+
when 'LSUB'
|
80
|
+
decoder.lsub(tag, *opt_args) {|res| response_write.call(res) }
|
81
|
+
when 'STATUS'
|
82
|
+
decoder.status(tag, *opt_args) {|res| response_write.call(res) }
|
83
|
+
when 'APPEND'
|
84
|
+
decoder.append(tag, *opt_args) {|res| response_write.call(res) }
|
85
|
+
when 'CHECK'
|
86
|
+
decoder.check(tag, *opt_args) {|res| response_write.call(res) }
|
87
|
+
when 'CLOSE'
|
88
|
+
decoder.close(tag, *opt_args) {|res| response_write.call(res) }
|
89
|
+
when 'EXPUNGE'
|
90
|
+
decoder.expunge(tag, *opt_args) {|res| response_write.call(res) }
|
91
|
+
when 'SEARCH'
|
92
|
+
decoder.search(tag, *opt_args) {|res| response_write.call(res) }
|
93
|
+
when 'FETCH'
|
94
|
+
decoder.fetch(tag, *opt_args) {|res| response_write.call(res) }
|
95
|
+
when 'STORE'
|
96
|
+
decoder.store(tag, *opt_args) {|res| response_write.call(res) }
|
97
|
+
when 'COPY'
|
98
|
+
decoder.copy(tag, *opt_args) {|res| response_write.call(res) }
|
99
|
+
when 'IDLE'
|
100
|
+
decoder.idle(tag, input, output, *opt_args) {|res| response_write.call(res) }
|
101
|
+
when 'UID'
|
102
|
+
unless (opt_args.empty?) then
|
103
|
+
uid_command, *uid_args = opt_args
|
104
|
+
logger.info("uid command: #{uid_command}")
|
105
|
+
logger.debug("uid parameter: #{uid_args}") if logger.debug?
|
106
|
+
case (uid_command.upcase)
|
107
|
+
when 'SEARCH'
|
108
|
+
decoder.search(tag, *uid_args, uid: true) {|res| response_write.call(res) }
|
109
|
+
when 'FETCH'
|
110
|
+
decoder.fetch(tag, *uid_args, uid: true) {|res| response_write.call(res) }
|
111
|
+
when 'STORE'
|
112
|
+
decoder.store(tag, *uid_args, uid: true) {|res| response_write.call(res) }
|
113
|
+
when 'COPY'
|
114
|
+
decoder.copy(tag, *uid_args, uid: true) {|res| response_write.call(res) }
|
115
|
+
else
|
116
|
+
logger.error("unknown uid command: #{uid_command}")
|
117
|
+
response_write.call([ "#{tag} BAD unknown uid command\r\n" ])
|
118
|
+
end
|
119
|
+
else
|
120
|
+
logger.error('empty uid parameter.')
|
121
|
+
response_write.call([ "#{tag} BAD empty uid parameter\r\n" ])
|
122
|
+
end
|
123
|
+
else
|
124
|
+
logger.error("unknown command: #{command}")
|
125
|
+
response_write.call([ "#{tag} BAD unknown command\r\n" ])
|
126
|
+
end
|
127
|
+
rescue
|
128
|
+
logger.error('unexpected error.')
|
129
|
+
logger.error($!)
|
130
|
+
response_write.call([ "#{tag} BAD unexpected error\r\n" ])
|
131
|
+
end
|
132
|
+
|
133
|
+
if (command.upcase == 'LOGOUT') then
|
134
|
+
break
|
135
|
+
end
|
136
|
+
|
137
|
+
decoder = decoder.next_decoder
|
138
|
+
end
|
139
|
+
|
140
|
+
nil
|
141
|
+
ensure
|
142
|
+
Error.suppress_2nd_error_at_resource_closing(logger: logger) { decoder.cleanup }
|
143
|
+
end
|
144
|
+
|
145
|
+
def initialize(auth, logger)
|
146
|
+
@auth = auth
|
147
|
+
@logger = logger
|
148
|
+
end
|
149
|
+
|
150
|
+
def response_stream(tag)
|
151
|
+
Enumerator.new{|res|
|
152
|
+
begin
|
153
|
+
yield(res)
|
154
|
+
rescue SyntaxError
|
155
|
+
@logger.error('client command syntax error.')
|
156
|
+
@logger.error($!)
|
157
|
+
res << "#{tag} BAD client command syntax error\r\n"
|
158
|
+
rescue
|
159
|
+
raise if ($!.name =~ /AssertionFailedError/)
|
160
|
+
@logger.error('internal server error.')
|
161
|
+
@logger.error($!)
|
162
|
+
res << "#{tag} BAD internal server error\r\n"
|
163
|
+
end
|
164
|
+
}
|
165
|
+
end
|
166
|
+
private :response_stream
|
167
|
+
|
168
|
+
def guard_error(tag, imap_command, *args, **name_args)
|
169
|
+
begin
|
170
|
+
if (name_args.empty?) then
|
171
|
+
__send__(imap_command, tag, *args) {|res| yield(res) }
|
172
|
+
else
|
173
|
+
__send__(imap_command, tag, *args, **name_args) {|res| yield(res) }
|
174
|
+
end
|
175
|
+
rescue SyntaxError
|
176
|
+
@logger.error('client command syntax error.')
|
177
|
+
@logger.error($!)
|
178
|
+
yield([ "#{tag} BAD client command syntax error\r\n" ])
|
179
|
+
rescue ArgumentError
|
180
|
+
@logger.error('invalid command parameter.')
|
181
|
+
@logger.error($!)
|
182
|
+
yield([ "#{tag} BAD invalid command parameter\r\n" ])
|
183
|
+
rescue
|
184
|
+
raise if ($!.class.name =~ /AssertionFailedError/)
|
185
|
+
@logger.error('internal server error.')
|
186
|
+
@logger.error($!)
|
187
|
+
yield([ "#{tag} BAD internal server error\r\n" ])
|
188
|
+
end
|
189
|
+
end
|
190
|
+
private :guard_error
|
191
|
+
|
192
|
+
class << self
|
193
|
+
def imap_command(name)
|
194
|
+
orig_name = "_#{name}".to_sym
|
195
|
+
alias_method orig_name, name
|
196
|
+
define_method name, lambda{|tag, *args, **name_args, &block|
|
197
|
+
guard_error(tag, orig_name, *args, **name_args, &block)
|
198
|
+
}
|
199
|
+
name.to_sym
|
200
|
+
end
|
201
|
+
private :imap_command
|
202
|
+
|
203
|
+
def fetch_mail_store_holder_and_on_demand_recovery(mail_store_pool, username,
|
204
|
+
write_lock_timeout_seconds: ReadWriteLock::DEFAULT_TIMEOUT_SECONDS,
|
205
|
+
logger: Logger.new(STDOUT))
|
206
|
+
unique_user_id = Authentication.unique_user_id(username)
|
207
|
+
logger.debug("unique user ID: #{username} -> #{unique_user_id}") if logger.debug?
|
208
|
+
|
209
|
+
mail_store_holder = mail_store_pool.get(unique_user_id, timeout_seconds: write_lock_timeout_seconds) {
|
210
|
+
logger.info("open mail store: #{unique_user_id} [ #{username} ]")
|
211
|
+
}
|
212
|
+
|
213
|
+
mail_store_holder.write_synchronize(write_lock_timeout_seconds) {
|
214
|
+
if (mail_store_holder.mail_store.abort_transaction?) then
|
215
|
+
logger.warn("user data recovery start: #{username}")
|
216
|
+
yield("* OK [ALERT] start user data recovery.\r\n")
|
217
|
+
mail_store_holder.mail_store.recovery_data(logger: logger).sync
|
218
|
+
logger.warn("user data recovery end: #{username}")
|
219
|
+
yield("* OK completed user data recovery.\r\n")
|
220
|
+
end
|
221
|
+
}
|
222
|
+
|
223
|
+
mail_store_holder
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def ok_greeting
|
228
|
+
yield([ "* OK RIMS v#{VERSION} IMAP4rev1 service ready.\r\n" ])
|
229
|
+
end
|
230
|
+
|
231
|
+
def capability(tag)
|
232
|
+
capability_list = %w[ IMAP4rev1 UIDPLUS IDLE ]
|
233
|
+
capability_list += @auth.capability.map{|auth_capability| "AUTH=#{auth_capability}" }
|
234
|
+
res = []
|
235
|
+
res << "* CAPABILITY #{capability_list.join(' ')}\r\n"
|
236
|
+
res << "#{tag} OK CAPABILITY completed\r\n"
|
237
|
+
yield(res)
|
238
|
+
end
|
239
|
+
imap_command :capability
|
240
|
+
|
241
|
+
def next_decoder
|
242
|
+
self
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
class InitialDecoder < Decoder
|
247
|
+
def initialize(mail_store_pool, auth, logger,
|
248
|
+
mail_delivery_user: Server::DEFAULT[:mail_delivery_user],
|
249
|
+
write_lock_timeout_seconds: ReadWriteLock::DEFAULT_TIMEOUT_SECONDS,
|
250
|
+
**next_decoder_optional)
|
251
|
+
super(auth, logger)
|
252
|
+
@next_decoder = self
|
253
|
+
@mail_store_pool = mail_store_pool
|
254
|
+
@folder = nil
|
255
|
+
@auth = auth
|
256
|
+
@mail_delivery_user = mail_delivery_user
|
257
|
+
@write_lock_timeout_seconds = write_lock_timeout_seconds
|
258
|
+
@next_decoder_optional = next_decoder_optional
|
259
|
+
end
|
260
|
+
|
261
|
+
attr_reader :next_decoder
|
262
|
+
|
263
|
+
def auth?
|
264
|
+
false
|
265
|
+
end
|
266
|
+
|
267
|
+
def selected?
|
268
|
+
false
|
269
|
+
end
|
270
|
+
|
271
|
+
def cleanup
|
272
|
+
nil
|
273
|
+
end
|
274
|
+
|
275
|
+
def not_authenticated_response(tag)
|
276
|
+
[ "#{tag} NO not authenticated\r\n" ]
|
277
|
+
end
|
278
|
+
private :not_authenticated_response
|
279
|
+
|
280
|
+
def noop(tag)
|
281
|
+
yield([ "#{tag} OK NOOP completed\r\n" ])
|
282
|
+
end
|
283
|
+
imap_command :noop
|
284
|
+
|
285
|
+
def logout(tag)
|
286
|
+
cleanup
|
287
|
+
res = []
|
288
|
+
res << "* BYE server logout\r\n"
|
289
|
+
res << "#{tag} OK LOGOUT completed\r\n"
|
290
|
+
yield(res)
|
291
|
+
end
|
292
|
+
imap_command :logout
|
293
|
+
|
294
|
+
def accept_authentication(username)
|
295
|
+
cleanup
|
296
|
+
|
297
|
+
case (username)
|
298
|
+
when @mail_delivery_user
|
299
|
+
@logger.info("mail delivery user: #{username}")
|
300
|
+
MailDeliveryDecoder.new(@mail_store_pool, @auth, @logger,
|
301
|
+
write_lock_timeout_seconds: @write_lock_timeout_seconds,
|
302
|
+
**@next_decoder_optional)
|
303
|
+
else
|
304
|
+
mail_store_holder =
|
305
|
+
self.class.fetch_mail_store_holder_and_on_demand_recovery(@mail_store_pool, username,
|
306
|
+
write_lock_timeout_seconds: @write_lock_timeout_seconds,
|
307
|
+
logger: @logger) {|msg| yield(msg) }
|
308
|
+
UserMailboxDecoder.new(self, mail_store_holder, @auth, @logger,
|
309
|
+
write_lock_timeout_seconds: @write_lock_timeout_seconds,
|
310
|
+
**@next_decoder_optional)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
private :accept_authentication
|
314
|
+
|
315
|
+
def authenticate(tag, client_response_input_stream, server_challenge_output_stream,
|
316
|
+
auth_type, inline_client_response_data_base64=nil)
|
317
|
+
auth_reader = AuthenticationReader.new(@auth, client_response_input_stream, server_challenge_output_stream, @logger)
|
318
|
+
if (username = auth_reader.authenticate_client(auth_type, inline_client_response_data_base64)) then
|
319
|
+
if (username != :*) then
|
320
|
+
yield response_stream(tag) {|res|
|
321
|
+
@logger.info("authentication OK: #{username}")
|
322
|
+
@next_decoder = accept_authentication(username) {|msg| res << msg }
|
323
|
+
res << "#{tag} OK AUTHENTICATE #{auth_type} success\r\n"
|
324
|
+
}
|
325
|
+
else
|
326
|
+
@logger.info('bad authentication.')
|
327
|
+
yield([ "#{tag} BAD AUTHENTICATE failed\r\n" ])
|
328
|
+
end
|
329
|
+
else
|
330
|
+
yield([ "#{tag} NO authentication failed\r\n" ])
|
331
|
+
end
|
332
|
+
end
|
333
|
+
imap_command :authenticate
|
334
|
+
|
335
|
+
def login(tag, username, password)
|
336
|
+
if (@auth.authenticate_login(username, password)) then
|
337
|
+
yield response_stream(tag) {|res|
|
338
|
+
@logger.info("login authentication OK: #{username}")
|
339
|
+
@next_decoder = accept_authentication(username) {|msg| res << msg }
|
340
|
+
res << "#{tag} OK LOGIN completed\r\n"
|
341
|
+
}
|
342
|
+
else
|
343
|
+
yield([ "#{tag} NO failed to login\r\n" ])
|
344
|
+
end
|
345
|
+
end
|
346
|
+
imap_command :login
|
347
|
+
|
348
|
+
def select(tag, mbox_name)
|
349
|
+
yield(not_authenticated_response(tag))
|
350
|
+
end
|
351
|
+
imap_command :select
|
352
|
+
|
353
|
+
def examine(tag, mbox_name)
|
354
|
+
yield(not_authenticated_response(tag))
|
355
|
+
end
|
356
|
+
imap_command :examine
|
357
|
+
|
358
|
+
def create(tag, mbox_name)
|
359
|
+
yield(not_authenticated_response(tag))
|
360
|
+
end
|
361
|
+
imap_command :create
|
362
|
+
|
363
|
+
def delete(tag, mbox_name)
|
364
|
+
yield(not_authenticated_response(tag))
|
365
|
+
end
|
366
|
+
imap_command :delete
|
367
|
+
|
368
|
+
def rename(tag, src_name, dst_name)
|
369
|
+
yield(not_authenticated_response(tag))
|
370
|
+
end
|
371
|
+
imap_command :rename
|
372
|
+
|
373
|
+
def subscribe(tag, mbox_name)
|
374
|
+
yield(not_authenticated_response(tag))
|
375
|
+
end
|
376
|
+
imap_command :subscribe
|
377
|
+
|
378
|
+
def unsubscribe(tag, mbox_name)
|
379
|
+
yield(not_authenticated_response(tag))
|
380
|
+
end
|
381
|
+
imap_command :unsubscribe
|
382
|
+
|
383
|
+
def list(tag, ref_name, mbox_name)
|
384
|
+
yield(not_authenticated_response(tag))
|
385
|
+
end
|
386
|
+
imap_command :list
|
387
|
+
|
388
|
+
def lsub(tag, ref_name, mbox_name)
|
389
|
+
yield(not_authenticated_response(tag))
|
390
|
+
end
|
391
|
+
imap_command :lsub
|
392
|
+
|
393
|
+
def status(tag, mbox_name, data_item_group)
|
394
|
+
yield(not_authenticated_response(tag))
|
395
|
+
end
|
396
|
+
imap_command :status
|
397
|
+
|
398
|
+
def append(tag, mbox_name, *opt_args, msg_text)
|
399
|
+
yield(not_authenticated_response(tag))
|
400
|
+
end
|
401
|
+
imap_command :append
|
402
|
+
|
403
|
+
def check(tag)
|
404
|
+
yield(not_authenticated_response(tag))
|
405
|
+
end
|
406
|
+
imap_command :check
|
407
|
+
|
408
|
+
def close(tag)
|
409
|
+
yield(not_authenticated_response(tag))
|
410
|
+
end
|
411
|
+
imap_command :close
|
412
|
+
|
413
|
+
def expunge(tag)
|
414
|
+
yield(not_authenticated_response(tag))
|
415
|
+
end
|
416
|
+
imap_command :expunge
|
417
|
+
|
418
|
+
def search(tag, *cond_args, uid: false)
|
419
|
+
yield(not_authenticated_response(tag))
|
420
|
+
end
|
421
|
+
imap_command :search
|
422
|
+
|
423
|
+
def fetch(tag, msg_set, data_item_group, uid: false)
|
424
|
+
yield(not_authenticated_response(tag))
|
425
|
+
end
|
426
|
+
imap_command :fetch
|
427
|
+
|
428
|
+
def store(tag, msg_set, data_item_name, data_item_value, uid: false)
|
429
|
+
yield(not_authenticated_response(tag))
|
430
|
+
end
|
431
|
+
imap_command :store
|
432
|
+
|
433
|
+
def copy(tag, msg_set, mbox_name, uid: false)
|
434
|
+
yield(not_authenticated_response(tag))
|
435
|
+
end
|
436
|
+
imap_command :copy
|
437
|
+
|
438
|
+
def idle(tag, client_input_stream, server_output_stream)
|
439
|
+
yield(not_authenticated_response(tag))
|
440
|
+
end
|
441
|
+
imap_command :idle
|
442
|
+
end
|
443
|
+
|
444
|
+
class AuthenticatedDecoder < Decoder
|
445
|
+
def authenticate(tag, client_response_input_stream, server_challenge_output_stream,
|
446
|
+
auth_type, inline_client_response_data_base64=nil, &block)
|
447
|
+
yield([ "#{tag} NO duplicated authentication\r\n" ])
|
448
|
+
end
|
449
|
+
imap_command :authenticate
|
450
|
+
|
451
|
+
def login(tag, username, password, &block)
|
452
|
+
yield([ "#{tag} NO duplicated login\r\n" ])
|
453
|
+
end
|
454
|
+
imap_command :login
|
455
|
+
end
|
456
|
+
|
457
|
+
class UserMailboxDecoder < AuthenticatedDecoder
|
458
|
+
def initialize(parent_decoder, mail_store_holder, auth, logger,
|
459
|
+
read_lock_timeout_seconds: ReadWriteLock::DEFAULT_TIMEOUT_SECONDS,
|
460
|
+
write_lock_timeout_seconds: ReadWriteLock::DEFAULT_TIMEOUT_SECONDS,
|
461
|
+
cleanup_write_lock_timeout_seconds: 1)
|
462
|
+
super(auth, logger)
|
463
|
+
@parent_decoder = parent_decoder
|
464
|
+
@mail_store_holder = mail_store_holder
|
465
|
+
@read_lock_timeout_seconds = read_lock_timeout_seconds
|
466
|
+
@write_lock_timeout_seconds = write_lock_timeout_seconds
|
467
|
+
@cleanup_write_lock_timeout_seconds = cleanup_write_lock_timeout_seconds
|
468
|
+
@folder = nil
|
469
|
+
end
|
470
|
+
|
471
|
+
def get_mail_store
|
472
|
+
@mail_store_holder.mail_store
|
473
|
+
end
|
474
|
+
private :get_mail_store
|
475
|
+
|
476
|
+
def auth?
|
477
|
+
@mail_store_holder != nil
|
478
|
+
end
|
479
|
+
|
480
|
+
def selected?
|
481
|
+
@folder != nil
|
482
|
+
end
|
483
|
+
|
484
|
+
def alive_folder?
|
485
|
+
get_mail_store.mbox_name(@folder.mbox_id) != nil
|
486
|
+
end
|
487
|
+
private :alive_folder?
|
488
|
+
|
489
|
+
def close_folder(&block)
|
490
|
+
if (auth? && selected? && alive_folder?) then
|
491
|
+
@folder.reload if @folder.updated?
|
492
|
+
@folder.close(&block)
|
493
|
+
@folder = nil
|
494
|
+
end
|
495
|
+
|
496
|
+
nil
|
497
|
+
end
|
498
|
+
private :close_folder
|
499
|
+
|
500
|
+
def cleanup
|
501
|
+
unless (@mail_store_holder.nil?) then
|
502
|
+
begin
|
503
|
+
@mail_store_holder.write_synchronize(@cleanup_write_lock_timeout_seconds) {
|
504
|
+
close_folder
|
505
|
+
@mail_store_holder.mail_store.sync
|
506
|
+
}
|
507
|
+
rescue WriteLockTimeoutError
|
508
|
+
@logger.warn("give up to close folder becaue of write-lock timeout over #{@write_lock_timeout_seconds} seconds")
|
509
|
+
@folder = nil
|
510
|
+
end
|
511
|
+
tmp_mail_store_holder = @mail_store_holder
|
512
|
+
ReadWriteLock.write_lock_timeout_detach(@cleanup_write_lock_timeout_seconds, @write_lock_timeout_seconds, logger: @logger) {|timeout_seconds|
|
513
|
+
tmp_mail_store_holder.return_pool(timeout_seconds: timeout_seconds) {
|
514
|
+
@logger.info("close mail store: #{tmp_mail_store_holder.unique_user_id}")
|
515
|
+
}
|
516
|
+
}
|
517
|
+
@mail_store_holder = nil
|
518
|
+
end
|
519
|
+
|
520
|
+
unless (@parent_decoder.nil?) then
|
521
|
+
@parent_decoder.cleanup
|
522
|
+
@parent_decoder = nil
|
523
|
+
end
|
524
|
+
|
525
|
+
nil
|
526
|
+
end
|
527
|
+
|
528
|
+
def should_be_alive_folder
|
529
|
+
alive_folder? or raise "deleted folder: #{@folder.mbox_id}"
|
530
|
+
end
|
531
|
+
private :should_be_alive_folder
|
532
|
+
|
533
|
+
def guard_authenticated(tag, imap_command, *args, exclusive: false, **name_args)
|
534
|
+
if (auth?) then
|
535
|
+
if (exclusive.nil?) then
|
536
|
+
guard_error(tag, imap_command, *args, **name_args) {|res|
|
537
|
+
yield(res)
|
538
|
+
}
|
539
|
+
else
|
540
|
+
begin
|
541
|
+
if (exclusive) then
|
542
|
+
@mail_store_holder.write_synchronize(@write_lock_timeout_seconds) {
|
543
|
+
guard_authenticated(tag, imap_command, *args, exclusive: nil, **name_args) {|res|
|
544
|
+
yield(res)
|
545
|
+
}
|
546
|
+
}
|
547
|
+
else
|
548
|
+
@mail_store_holder.read_synchronize(@read_lock_timeout_seconds){
|
549
|
+
guard_authenticated(tag, imap_command, *args, exclusive: nil, **name_args) {|res|
|
550
|
+
yield(res)
|
551
|
+
}
|
552
|
+
}
|
553
|
+
end
|
554
|
+
rescue ReadLockTimeoutError
|
555
|
+
@logger.error("write-lock timeout over #{@write_lock_timeout_seconds} seconds")
|
556
|
+
yield([ "#{tag} BAD write-lock timeout over #{@write_lock_timeout_seconds} seconds" ])
|
557
|
+
rescue WriteLockTimeoutError
|
558
|
+
@logger.error("read-lock timeout over #{@read_lock_timeout_seconds} seconds")
|
559
|
+
yield([ "#{tag} BAD read-lock timeout over #{@read_lock_timeout_seconds} seconds" ])
|
560
|
+
end
|
561
|
+
end
|
562
|
+
else
|
563
|
+
yield([ "#{tag} NO not authenticated\r\n" ])
|
564
|
+
end
|
565
|
+
end
|
566
|
+
private :guard_authenticated
|
567
|
+
|
568
|
+
def guard_selected(tag, imap_command, *args, **name_args)
|
569
|
+
if (selected?) then
|
570
|
+
guard_authenticated(tag, imap_command, *args, **name_args) {|res|
|
571
|
+
yield(res)
|
572
|
+
}
|
573
|
+
else
|
574
|
+
yield([ "#{tag} NO not selected\r\n" ])
|
575
|
+
end
|
576
|
+
end
|
577
|
+
private :guard_selected
|
578
|
+
|
579
|
+
class << self
|
580
|
+
def imap_command_authenticated(name, **guard_optional)
|
581
|
+
orig_name = "_#{name}".to_sym
|
582
|
+
alias_method orig_name, name
|
583
|
+
define_method name, lambda{|tag, *args, **name_args, &block|
|
584
|
+
guard_authenticated(tag, orig_name, *args, **name_args.merge(guard_optional), &block)
|
585
|
+
}
|
586
|
+
name.to_sym
|
587
|
+
end
|
588
|
+
private :imap_command_authenticated
|
589
|
+
|
590
|
+
def imap_command_selected(name, **guard_optional)
|
591
|
+
orig_name = "_#{name}".to_sym
|
592
|
+
alias_method orig_name, name
|
593
|
+
define_method name, lambda{|tag, *args, **name_args, &block|
|
594
|
+
guard_selected(tag, orig_name, *args, **name_args.merge(guard_optional), &block)
|
595
|
+
}
|
596
|
+
name.to_sym
|
597
|
+
end
|
598
|
+
private :imap_command_selected
|
599
|
+
end
|
600
|
+
|
601
|
+
def noop(tag)
|
602
|
+
res = []
|
603
|
+
if (auth? && selected?) then
|
604
|
+
begin
|
605
|
+
@mail_store_holder.read_synchronize(@read_lock_timeout_seconds) {
|
606
|
+
@folder.server_response_fetch{|r| res << r } if @folder.server_response?
|
607
|
+
}
|
608
|
+
rescue ReadLockTimeoutError
|
609
|
+
@logger.warn("give up to get folder status because of read-lock timeout over #{@read_lock_timeout_seconds} seconds")
|
610
|
+
end
|
611
|
+
end
|
612
|
+
res << "#{tag} OK NOOP completed\r\n"
|
613
|
+
yield(res)
|
614
|
+
end
|
615
|
+
imap_command :noop
|
616
|
+
|
617
|
+
def logout(tag)
|
618
|
+
cleanup
|
619
|
+
res = []
|
620
|
+
res << "* BYE server logout\r\n"
|
621
|
+
res << "#{tag} OK LOGOUT completed\r\n"
|
622
|
+
yield(res)
|
623
|
+
end
|
624
|
+
imap_command :logout
|
625
|
+
|
626
|
+
def folder_open_msgs
|
627
|
+
all_msgs = get_mail_store.mbox_msg_num(@folder.mbox_id)
|
628
|
+
recent_msgs = get_mail_store.mbox_flag_num(@folder.mbox_id, 'recent')
|
629
|
+
unseen_msgs = all_msgs - get_mail_store.mbox_flag_num(@folder.mbox_id, 'seen')
|
630
|
+
yield("* #{all_msgs} EXISTS\r\n")
|
631
|
+
yield("* #{recent_msgs} RECENT\r\n")
|
632
|
+
yield("* OK [UNSEEN #{unseen_msgs}]\r\n")
|
633
|
+
yield("* OK [UIDVALIDITY #{@folder.mbox_id}]\r\n")
|
634
|
+
yield("* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n")
|
635
|
+
nil
|
636
|
+
end
|
637
|
+
private :folder_open_msgs
|
638
|
+
|
639
|
+
def select(tag, mbox_name)
|
640
|
+
res = []
|
641
|
+
@folder = nil
|
642
|
+
mbox_name_utf8 = Net::IMAP.decode_utf7(mbox_name)
|
643
|
+
if (id = get_mail_store.mbox_id(mbox_name_utf8)) then
|
644
|
+
@folder = get_mail_store.select_mbox(id)
|
645
|
+
folder_open_msgs do |msg|
|
646
|
+
res << msg
|
647
|
+
end
|
648
|
+
res << "#{tag} OK [READ-WRITE] SELECT completed\r\n"
|
649
|
+
else
|
650
|
+
res << "#{tag} NO not found a mailbox\r\n"
|
651
|
+
end
|
652
|
+
yield(res)
|
653
|
+
end
|
654
|
+
imap_command_authenticated :select
|
655
|
+
|
656
|
+
def examine(tag, mbox_name)
|
657
|
+
res = []
|
658
|
+
@folder = nil
|
659
|
+
mbox_name_utf8 = Net::IMAP.decode_utf7(mbox_name)
|
660
|
+
if (id = get_mail_store.mbox_id(mbox_name_utf8)) then
|
661
|
+
@folder = get_mail_store.examine_mbox(id)
|
662
|
+
folder_open_msgs do |msg|
|
663
|
+
res << msg
|
664
|
+
end
|
665
|
+
res << "#{tag} OK [READ-ONLY] EXAMINE completed\r\n"
|
666
|
+
else
|
667
|
+
res << "#{tag} NO not found a mailbox\r\n"
|
668
|
+
end
|
669
|
+
yield(res)
|
670
|
+
end
|
671
|
+
imap_command_authenticated :examine
|
672
|
+
|
673
|
+
def create(tag, mbox_name)
|
674
|
+
res = []
|
675
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
676
|
+
mbox_name_utf8 = Net::IMAP.decode_utf7(mbox_name)
|
677
|
+
if (get_mail_store.mbox_id(mbox_name_utf8)) then
|
678
|
+
res << "#{tag} NO duplicated mailbox\r\n"
|
679
|
+
else
|
680
|
+
get_mail_store.add_mbox(mbox_name_utf8)
|
681
|
+
res << "#{tag} OK CREATE completed\r\n"
|
682
|
+
end
|
683
|
+
yield(res)
|
684
|
+
end
|
685
|
+
imap_command_authenticated :create, exclusive: true
|
686
|
+
|
687
|
+
def delete(tag, mbox_name)
|
688
|
+
res = []
|
689
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
690
|
+
mbox_name_utf8 = Net::IMAP.decode_utf7(mbox_name)
|
691
|
+
if (id = get_mail_store.mbox_id(mbox_name_utf8)) then
|
692
|
+
if (id != get_mail_store.mbox_id('INBOX')) then
|
693
|
+
get_mail_store.del_mbox(id)
|
694
|
+
res << "#{tag} OK DELETE completed\r\n"
|
695
|
+
else
|
696
|
+
res << "#{tag} NO not delete inbox\r\n"
|
697
|
+
end
|
698
|
+
else
|
699
|
+
res << "#{tag} NO not found a mailbox\r\n"
|
700
|
+
end
|
701
|
+
yield(res)
|
702
|
+
end
|
703
|
+
imap_command_authenticated :delete, exclusive: true
|
704
|
+
|
705
|
+
def rename(tag, src_name, dst_name)
|
706
|
+
res = []
|
707
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
708
|
+
src_name_utf8 = Net::IMAP.decode_utf7(src_name)
|
709
|
+
dst_name_utf8 = Net::IMAP.decode_utf7(dst_name)
|
710
|
+
unless (id = get_mail_store.mbox_id(src_name_utf8)) then
|
711
|
+
return yield(res << "#{tag} NO not found a mailbox\r\n")
|
712
|
+
end
|
713
|
+
if (id == get_mail_store.mbox_id('INBOX')) then
|
714
|
+
return yield(res << "#{tag} NO not rename inbox\r\n")
|
715
|
+
end
|
716
|
+
if (get_mail_store.mbox_id(dst_name_utf8)) then
|
717
|
+
return yield(res << "#{tag} NO duplicated mailbox\r\n")
|
718
|
+
end
|
719
|
+
get_mail_store.rename_mbox(id, dst_name_utf8)
|
720
|
+
return yield(res << "#{tag} OK RENAME completed\r\n")
|
721
|
+
end
|
722
|
+
imap_command_authenticated :rename, exclusive: true
|
723
|
+
|
724
|
+
def subscribe(tag, mbox_name)
|
725
|
+
res = []
|
726
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
727
|
+
mbox_name_utf8 = Net::IMAP.decode_utf7(mbox_name)
|
728
|
+
if (_mbox_id = get_mail_store.mbox_id(mbox_name_utf8)) then
|
729
|
+
res << "#{tag} OK SUBSCRIBE completed\r\n"
|
730
|
+
else
|
731
|
+
res << "#{tag} NO not found a mailbox\r\n"
|
732
|
+
end
|
733
|
+
yield(res)
|
734
|
+
end
|
735
|
+
imap_command_authenticated :subscribe
|
736
|
+
|
737
|
+
def unsubscribe(tag, mbox_name)
|
738
|
+
res = []
|
739
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
740
|
+
if (_mbox_id = get_mail_store.mbox_id(mbox_name)) then
|
741
|
+
res << "#{tag} NO not implemented subscribe/unsbscribe command\r\n"
|
742
|
+
else
|
743
|
+
res << "#{tag} NO not found a mailbox\r\n"
|
744
|
+
end
|
745
|
+
yield(res)
|
746
|
+
end
|
747
|
+
imap_command_authenticated :unsubscribe
|
748
|
+
|
749
|
+
def list_mbox(ref_name, mbox_name)
|
750
|
+
ref_name_utf8 = Net::IMAP.decode_utf7(ref_name)
|
751
|
+
mbox_name_utf8 = Net::IMAP.decode_utf7(mbox_name)
|
752
|
+
|
753
|
+
mbox_filter = Protocol.compile_wildcard(mbox_name_utf8)
|
754
|
+
mbox_list = get_mail_store.each_mbox_id.map{|id| [ id, get_mail_store.mbox_name(id) ] }
|
755
|
+
mbox_list.keep_if{|id, name| name.start_with? ref_name_utf8 }
|
756
|
+
mbox_list.keep_if{|id, name| name[(ref_name_utf8.length)..-1] =~ mbox_filter }
|
757
|
+
|
758
|
+
for id, name_utf8 in mbox_list
|
759
|
+
name = Net::IMAP.encode_utf7(name_utf8)
|
760
|
+
attrs = '\Noinferiors'
|
761
|
+
if (get_mail_store.mbox_flag_num(id, 'recent') > 0) then
|
762
|
+
attrs << ' \Marked'
|
763
|
+
else
|
764
|
+
attrs << ' \Unmarked'
|
765
|
+
end
|
766
|
+
yield("(#{attrs}) NIL #{Protocol.quote(name)}")
|
767
|
+
end
|
768
|
+
|
769
|
+
nil
|
770
|
+
end
|
771
|
+
private :list_mbox
|
772
|
+
|
773
|
+
def list(tag, ref_name, mbox_name)
|
774
|
+
res = []
|
775
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
776
|
+
if (mbox_name.empty?) then
|
777
|
+
res << "* LIST (\\Noselect) NIL \"\"\r\n"
|
778
|
+
else
|
779
|
+
list_mbox(ref_name, mbox_name) do |mbox_entry|
|
780
|
+
res << "* LIST #{mbox_entry}\r\n"
|
781
|
+
end
|
782
|
+
end
|
783
|
+
res << "#{tag} OK LIST completed\r\n"
|
784
|
+
yield(res)
|
785
|
+
end
|
786
|
+
imap_command_authenticated :list
|
787
|
+
|
788
|
+
def lsub(tag, ref_name, mbox_name)
|
789
|
+
res = []
|
790
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
791
|
+
if (mbox_name.empty?) then
|
792
|
+
res << "* LSUB (\\Noselect) NIL \"\"\r\n"
|
793
|
+
else
|
794
|
+
list_mbox(ref_name, mbox_name) do |mbox_entry|
|
795
|
+
res << "* LSUB #{mbox_entry}\r\n"
|
796
|
+
end
|
797
|
+
end
|
798
|
+
res << "#{tag} OK LSUB completed\r\n"
|
799
|
+
yield(res)
|
800
|
+
end
|
801
|
+
imap_command_authenticated :lsub
|
802
|
+
|
803
|
+
def status(tag, mbox_name, data_item_group)
|
804
|
+
res = []
|
805
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
806
|
+
mbox_name_utf8 = Net::IMAP.decode_utf7(mbox_name)
|
807
|
+
if (id = get_mail_store.mbox_id(mbox_name_utf8)) then
|
808
|
+
unless ((data_item_group.is_a? Array) && (data_item_group[0] == :group)) then
|
809
|
+
raise SyntaxError, 'second arugment is not a group list.'
|
810
|
+
end
|
811
|
+
|
812
|
+
values = []
|
813
|
+
for item in data_item_group[1..-1]
|
814
|
+
case (item.upcase)
|
815
|
+
when 'MESSAGES'
|
816
|
+
values << 'MESSAGES' << get_mail_store.mbox_msg_num(id)
|
817
|
+
when 'RECENT'
|
818
|
+
values << 'RECENT' << get_mail_store.mbox_flag_num(id, 'recent')
|
819
|
+
when 'UIDNEXT'
|
820
|
+
values << 'UIDNEXT' << get_mail_store.uid(id)
|
821
|
+
when 'UIDVALIDITY'
|
822
|
+
values << 'UIDVALIDITY' << id
|
823
|
+
when 'UNSEEN'
|
824
|
+
unseen_flags = get_mail_store.mbox_msg_num(id) - get_mail_store.mbox_flag_num(id, 'seen')
|
825
|
+
values << 'UNSEEN' << unseen_flags
|
826
|
+
else
|
827
|
+
raise SyntaxError, "unknown status data: #{item}"
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
res << "* STATUS #{Protocol.quote(mbox_name)} (#{values.join(' ')})\r\n"
|
832
|
+
res << "#{tag} OK STATUS completed\r\n"
|
833
|
+
else
|
834
|
+
res << "#{tag} NO not found a mailbox\r\n"
|
835
|
+
end
|
836
|
+
yield(res)
|
837
|
+
end
|
838
|
+
imap_command_authenticated :status
|
839
|
+
|
840
|
+
def mailbox_size_server_response_multicast_push(mbox_id)
|
841
|
+
all_msgs = get_mail_store.mbox_msg_num(mbox_id)
|
842
|
+
recent_msgs = get_mail_store.mbox_flag_num(mbox_id, 'recent')
|
843
|
+
|
844
|
+
f = get_mail_store.examine_mbox(mbox_id)
|
845
|
+
begin
|
846
|
+
f.server_response_multicast_push("* #{all_msgs} EXISTS\r\n")
|
847
|
+
f.server_response_multicast_push("* #{recent_msgs} RECENT\r\n")
|
848
|
+
ensure
|
849
|
+
f.close
|
850
|
+
end
|
851
|
+
|
852
|
+
nil
|
853
|
+
end
|
854
|
+
private :mailbox_size_server_response_multicast_push
|
855
|
+
|
856
|
+
def append(tag, mbox_name, *opt_args, msg_text)
|
857
|
+
res = []
|
858
|
+
mbox_name_utf8 = Net::IMAP.decode_utf7(mbox_name)
|
859
|
+
if (mbox_id = get_mail_store.mbox_id(mbox_name_utf8)) then
|
860
|
+
msg_flags = []
|
861
|
+
msg_date = Time.now
|
862
|
+
|
863
|
+
if ((! opt_args.empty?) && (opt_args[0].is_a? Array)) then
|
864
|
+
opt_flags = opt_args.shift
|
865
|
+
if (opt_flags[0] != :group) then
|
866
|
+
raise SyntaxError, 'bad flag list.'
|
867
|
+
end
|
868
|
+
for flag_atom in opt_flags[1..-1]
|
869
|
+
case (flag_atom.upcase)
|
870
|
+
when '\ANSWERED'
|
871
|
+
msg_flags << 'answered'
|
872
|
+
when '\FLAGGED'
|
873
|
+
msg_flags << 'flagged'
|
874
|
+
when '\DELETED'
|
875
|
+
msg_flags << 'deleted'
|
876
|
+
when '\SEEN'
|
877
|
+
msg_flags << 'seen'
|
878
|
+
when '\DRAFT'
|
879
|
+
msg_flags << 'draft'
|
880
|
+
else
|
881
|
+
raise SyntaxError, "invalid flag: #{flag_atom}"
|
882
|
+
end
|
883
|
+
end
|
884
|
+
end
|
885
|
+
|
886
|
+
if ((! opt_args.empty?) && (opt_args[0].is_a? String)) then
|
887
|
+
begin
|
888
|
+
msg_date = Time.parse(opt_args.shift)
|
889
|
+
rescue ArgumentError
|
890
|
+
raise SyntaxError, $!.message
|
891
|
+
end
|
892
|
+
end
|
893
|
+
|
894
|
+
unless (opt_args.empty?) then
|
895
|
+
raise SyntaxError, "unknown option: #{opt_args.inspect}"
|
896
|
+
end
|
897
|
+
|
898
|
+
uid = get_mail_store.add_msg(mbox_id, msg_text, msg_date)
|
899
|
+
for flag_name in msg_flags
|
900
|
+
get_mail_store.set_msg_flag(mbox_id, uid, flag_name, true)
|
901
|
+
end
|
902
|
+
mailbox_size_server_response_multicast_push(mbox_id)
|
903
|
+
|
904
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
905
|
+
res << "#{tag} OK [APPENDUID #{mbox_id} #{uid}] APPEND completed\r\n"
|
906
|
+
else
|
907
|
+
@folder.server_response_fetch{|r| res << r } if selected?
|
908
|
+
res << "#{tag} NO [TRYCREATE] not found a mailbox\r\n"
|
909
|
+
end
|
910
|
+
yield(res)
|
911
|
+
end
|
912
|
+
imap_command_authenticated :append, exclusive: true
|
913
|
+
|
914
|
+
def check(tag)
|
915
|
+
res = []
|
916
|
+
@folder.server_response_fetch{|r| res << r }
|
917
|
+
get_mail_store.sync
|
918
|
+
res << "#{tag} OK CHECK completed\r\n"
|
919
|
+
yield(res)
|
920
|
+
end
|
921
|
+
imap_command_selected :check, exclusive: true
|
922
|
+
|
923
|
+
def close(tag, &block)
|
924
|
+
yield response_stream(tag) {|res|
|
925
|
+
@folder.server_response_fetch{|r| res << r }
|
926
|
+
close_folder do |msg_num|
|
927
|
+
r = "* #{msg_num} EXPUNGE\r\n"
|
928
|
+
res << r
|
929
|
+
@folder.server_response_multicast_push(r)
|
930
|
+
end
|
931
|
+
get_mail_store.sync
|
932
|
+
res << "#{tag} OK CLOSE completed\r\n"
|
933
|
+
}
|
934
|
+
end
|
935
|
+
imap_command_selected :close, exclusive: true
|
936
|
+
|
937
|
+
def expunge(tag)
|
938
|
+
return yield([ "#{tag} NO cannot expunge in read-only mode\r\n" ]) if @folder.read_only?
|
939
|
+
should_be_alive_folder
|
940
|
+
@folder.reload if @folder.updated?
|
941
|
+
|
942
|
+
yield response_stream(tag) {|res|
|
943
|
+
@folder.server_response_fetch{|r| res << r }
|
944
|
+
@folder.expunge_mbox do |msg_num|
|
945
|
+
r = "* #{msg_num} EXPUNGE\r\n"
|
946
|
+
res << r
|
947
|
+
@folder.server_response_multicast_push(r)
|
948
|
+
end
|
949
|
+
res << "#{tag} OK EXPUNGE completed\r\n"
|
950
|
+
}
|
951
|
+
end
|
952
|
+
imap_command_selected :expunge, exclusive: true
|
953
|
+
|
954
|
+
def search(tag, *cond_args, uid: false)
|
955
|
+
should_be_alive_folder
|
956
|
+
@folder.reload if @folder.updated?
|
957
|
+
parser = Protocol::SearchParser.new(get_mail_store, @folder)
|
958
|
+
|
959
|
+
if (! cond_args.empty? && cond_args[0].upcase == 'CHARSET') then
|
960
|
+
cond_args.shift
|
961
|
+
charset_string = cond_args.shift or raise SyntaxError, 'need for a charset string of CHARSET'
|
962
|
+
charset_string.is_a? String or raise SyntaxError, "CHARSET charset string expected as <String> but was <#{charset_string.class}>."
|
963
|
+
parser.charset = charset_string
|
964
|
+
end
|
965
|
+
|
966
|
+
if (cond_args.empty?) then
|
967
|
+
raise SyntaxError, 'required search arguments.'
|
968
|
+
end
|
969
|
+
|
970
|
+
if (cond_args[0].upcase == 'UID' && cond_args.length >= 2) then
|
971
|
+
begin
|
972
|
+
msg_set = @folder.parse_msg_set(cond_args[1], uid: true)
|
973
|
+
msg_src = @folder.msg_find_all(msg_set, uid: true)
|
974
|
+
cond_args.shift(2)
|
975
|
+
rescue MessageSetSyntaxError
|
976
|
+
msg_src = @folder.each_msg
|
977
|
+
end
|
978
|
+
else
|
979
|
+
begin
|
980
|
+
msg_set = @folder.parse_msg_set(cond_args[0], uid: false)
|
981
|
+
msg_src = @folder.msg_find_all(msg_set, uid: false)
|
982
|
+
cond_args.shift
|
983
|
+
rescue MessageSetSyntaxError
|
984
|
+
msg_src = @folder.each_msg
|
985
|
+
end
|
986
|
+
end
|
987
|
+
cond = parser.parse(cond_args)
|
988
|
+
|
989
|
+
yield response_stream(tag) {|res|
|
990
|
+
@folder.server_response_fetch{|r| res << r }
|
991
|
+
res << '* SEARCH'
|
992
|
+
for msg in msg_src
|
993
|
+
if (cond.call(msg)) then
|
994
|
+
if (uid) then
|
995
|
+
res << " #{msg.uid}"
|
996
|
+
else
|
997
|
+
res << " #{msg.num}"
|
998
|
+
end
|
999
|
+
end
|
1000
|
+
end
|
1001
|
+
res << "\r\n"
|
1002
|
+
res << "#{tag} OK SEARCH completed\r\n"
|
1003
|
+
}
|
1004
|
+
end
|
1005
|
+
imap_command_selected :search
|
1006
|
+
|
1007
|
+
def fetch(tag, msg_set, data_item_group, uid: false)
|
1008
|
+
should_be_alive_folder
|
1009
|
+
@folder.reload if @folder.updated?
|
1010
|
+
|
1011
|
+
msg_set = @folder.parse_msg_set(msg_set, uid: uid)
|
1012
|
+
msg_list = @folder.msg_find_all(msg_set, uid: uid)
|
1013
|
+
|
1014
|
+
unless ((data_item_group.is_a? Array) && data_item_group[0] == :group) then
|
1015
|
+
data_item_group = [ :group, data_item_group ]
|
1016
|
+
end
|
1017
|
+
if (uid) then
|
1018
|
+
unless (data_item_group.find{|i| (i.is_a? String) && (i.upcase == 'UID') }) then
|
1019
|
+
data_item_group = [ :group, 'UID' ] + data_item_group[1..-1]
|
1020
|
+
end
|
1021
|
+
end
|
1022
|
+
|
1023
|
+
parser = Protocol::FetchParser.new(get_mail_store, @folder)
|
1024
|
+
fetch = parser.parse(data_item_group)
|
1025
|
+
|
1026
|
+
yield response_stream(tag) {|res|
|
1027
|
+
@folder.server_response_fetch{|r| res << r }
|
1028
|
+
for msg in msg_list
|
1029
|
+
res << ('* '.b << msg.num.to_s.b << ' FETCH '.b << fetch.call(msg) << "\r\n".b)
|
1030
|
+
end
|
1031
|
+
res << "#{tag} OK FETCH completed\r\n"
|
1032
|
+
}
|
1033
|
+
end
|
1034
|
+
imap_command_selected :fetch
|
1035
|
+
|
1036
|
+
def store(tag, msg_set, data_item_name, data_item_value, uid: false)
|
1037
|
+
return yield([ "#{tag} NO cannot store in read-only mode\r\n" ]) if @folder.read_only?
|
1038
|
+
should_be_alive_folder
|
1039
|
+
@folder.reload if @folder.updated?
|
1040
|
+
|
1041
|
+
msg_set = @folder.parse_msg_set(msg_set, uid: uid)
|
1042
|
+
name, option = data_item_name.split(/\./, 2)
|
1043
|
+
|
1044
|
+
case (name.upcase)
|
1045
|
+
when 'FLAGS'
|
1046
|
+
action = :flags_replace
|
1047
|
+
when '+FLAGS'
|
1048
|
+
action = :flags_add
|
1049
|
+
when '-FLAGS'
|
1050
|
+
action = :flags_del
|
1051
|
+
else
|
1052
|
+
raise SyntaxError, "unknown store action: #{name}"
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
case (option && option.upcase)
|
1056
|
+
when 'SILENT'
|
1057
|
+
is_silent = true
|
1058
|
+
when nil
|
1059
|
+
is_silent = false
|
1060
|
+
else
|
1061
|
+
raise SyntaxError, "unknown store option: #{option.inspect}"
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
if ((data_item_value.is_a? Array) && data_item_value[0] == :group) then
|
1065
|
+
flag_list = []
|
1066
|
+
for flag_atom in data_item_value[1..-1]
|
1067
|
+
case (flag_atom.upcase)
|
1068
|
+
when '\ANSWERED'
|
1069
|
+
flag_list << 'answered'
|
1070
|
+
when '\FLAGGED'
|
1071
|
+
flag_list << 'flagged'
|
1072
|
+
when '\DELETED'
|
1073
|
+
flag_list << 'deleted'
|
1074
|
+
when '\SEEN'
|
1075
|
+
flag_list << 'seen'
|
1076
|
+
when '\DRAFT'
|
1077
|
+
flag_list << 'draft'
|
1078
|
+
else
|
1079
|
+
raise SyntaxError, "invalid flag: #{flag_atom}"
|
1080
|
+
end
|
1081
|
+
end
|
1082
|
+
rest_flag_list = (MailStore::MSG_FLAG_NAMES - %w[ recent ]) - flag_list
|
1083
|
+
else
|
1084
|
+
raise SyntaxError, 'third arugment is not a group list.'
|
1085
|
+
end
|
1086
|
+
|
1087
|
+
msg_list = @folder.msg_find_all(msg_set, uid: uid)
|
1088
|
+
|
1089
|
+
for msg in msg_list
|
1090
|
+
case (action)
|
1091
|
+
when :flags_replace
|
1092
|
+
for name in flag_list
|
1093
|
+
get_mail_store.set_msg_flag(@folder.mbox_id, msg.uid, name, true)
|
1094
|
+
end
|
1095
|
+
for name in rest_flag_list
|
1096
|
+
get_mail_store.set_msg_flag(@folder.mbox_id, msg.uid, name, false)
|
1097
|
+
end
|
1098
|
+
when :flags_add
|
1099
|
+
for name in flag_list
|
1100
|
+
get_mail_store.set_msg_flag(@folder.mbox_id, msg.uid, name, true)
|
1101
|
+
end
|
1102
|
+
when :flags_del
|
1103
|
+
for name in flag_list
|
1104
|
+
get_mail_store.set_msg_flag(@folder.mbox_id, msg.uid, name, false)
|
1105
|
+
end
|
1106
|
+
else
|
1107
|
+
raise "internal error: unknown action: #{action}"
|
1108
|
+
end
|
1109
|
+
end
|
1110
|
+
|
1111
|
+
if (is_silent) then
|
1112
|
+
silent_res = []
|
1113
|
+
@folder.server_response_fetch{|r| silent_res << r }
|
1114
|
+
silent_res << "#{tag} OK STORE completed\r\n"
|
1115
|
+
yield(silent_res)
|
1116
|
+
else
|
1117
|
+
yield response_stream(tag) {|res|
|
1118
|
+
@folder.server_response_fetch{|r| res << r }
|
1119
|
+
for msg in msg_list
|
1120
|
+
flag_atom_list = nil
|
1121
|
+
|
1122
|
+
if (get_mail_store.msg_exist? @folder.mbox_id, msg.uid) then
|
1123
|
+
flag_atom_list = []
|
1124
|
+
for name in MailStore::MSG_FLAG_NAMES
|
1125
|
+
if (get_mail_store.msg_flag(@folder.mbox_id, msg.uid, name)) then
|
1126
|
+
flag_atom_list << "\\#{name.capitalize}"
|
1127
|
+
end
|
1128
|
+
end
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
if (flag_atom_list) then
|
1132
|
+
if (uid) then
|
1133
|
+
res << "* #{msg.num} FETCH (UID #{msg.uid} FLAGS (#{flag_atom_list.join(' ')}))\r\n"
|
1134
|
+
else
|
1135
|
+
res << "* #{msg.num} FETCH (FLAGS (#{flag_atom_list.join(' ')}))\r\n"
|
1136
|
+
end
|
1137
|
+
else
|
1138
|
+
@logger.warn("not found a message and skipped: uidvalidity(#{@folder.mbox_id}) uid(#{msg.uid})")
|
1139
|
+
end
|
1140
|
+
end
|
1141
|
+
res << "#{tag} OK STORE completed\r\n"
|
1142
|
+
}
|
1143
|
+
end
|
1144
|
+
end
|
1145
|
+
imap_command_selected :store, exclusive: true
|
1146
|
+
|
1147
|
+
def copy(tag, msg_set, mbox_name, uid: false)
|
1148
|
+
should_be_alive_folder
|
1149
|
+
@folder.reload if @folder.updated?
|
1150
|
+
|
1151
|
+
res = []
|
1152
|
+
mbox_name_utf8 = Net::IMAP.decode_utf7(mbox_name)
|
1153
|
+
msg_set = @folder.parse_msg_set(msg_set, uid: uid)
|
1154
|
+
|
1155
|
+
if (mbox_id = get_mail_store.mbox_id(mbox_name_utf8)) then
|
1156
|
+
msg_list = @folder.msg_find_all(msg_set, uid: uid)
|
1157
|
+
|
1158
|
+
src_uids = []
|
1159
|
+
dst_uids = []
|
1160
|
+
for msg in msg_list
|
1161
|
+
src_uids << msg.uid
|
1162
|
+
dst_uids << get_mail_store.copy_msg(msg.uid, @folder.mbox_id, mbox_id)
|
1163
|
+
end
|
1164
|
+
|
1165
|
+
if msg_list.size > 0
|
1166
|
+
mailbox_size_server_response_multicast_push(mbox_id)
|
1167
|
+
@folder.server_response_fetch{|r| res << r }
|
1168
|
+
res << "#{tag} OK [COPYUID #{mbox_id} #{src_uids.join(',')} #{dst_uids.join(',')}] COPY completed\r\n"
|
1169
|
+
else
|
1170
|
+
@folder.server_response_fetch{|r| res << r }
|
1171
|
+
res << "#{tag} OK COPY completed\r\n"
|
1172
|
+
end
|
1173
|
+
else
|
1174
|
+
@folder.server_response_fetch{|r| res << r }
|
1175
|
+
res << "#{tag} NO [TRYCREATE] not found a mailbox\r\n"
|
1176
|
+
end
|
1177
|
+
yield(res)
|
1178
|
+
end
|
1179
|
+
imap_command_selected :copy, exclusive: true
|
1180
|
+
|
1181
|
+
def idle(tag, client_input_stream, server_output_stream)
|
1182
|
+
@logger.info('idle start...')
|
1183
|
+
server_output_stream.write("+ continue\r\n")
|
1184
|
+
server_output_stream.flush
|
1185
|
+
|
1186
|
+
server_response_thread = Thread.new{
|
1187
|
+
@logger.info('idle server response thread start... ')
|
1188
|
+
@folder.server_response_idle_wait{|server_response_list|
|
1189
|
+
@logger.debug("idle server response: #{server_response}") if @logger.debug?
|
1190
|
+
for server_response in server_response_list
|
1191
|
+
server_output_stream.write(server_response)
|
1192
|
+
end
|
1193
|
+
server_output_stream.flush
|
1194
|
+
}
|
1195
|
+
server_output_stream.flush
|
1196
|
+
@logger.info('idle server response thread terminated.')
|
1197
|
+
}
|
1198
|
+
|
1199
|
+
begin
|
1200
|
+
line = client_input_stream.gets
|
1201
|
+
ensure
|
1202
|
+
@folder.server_response_idle_interrupt
|
1203
|
+
server_response_thread.join
|
1204
|
+
end
|
1205
|
+
|
1206
|
+
res = []
|
1207
|
+
if (line) then
|
1208
|
+
line.chomp!("\n")
|
1209
|
+
line.chomp!("\r")
|
1210
|
+
if (line.upcase == "DONE") then
|
1211
|
+
@logger.info('idle terminated.')
|
1212
|
+
res << "#{tag} OK IDLE terminated\r\n"
|
1213
|
+
else
|
1214
|
+
@logger.warn('unexpected client response and idle terminated.')
|
1215
|
+
@logger.debug("unexpected client response data: #{line}")
|
1216
|
+
res << "#{tag} BAD unexpected client response\r\n"
|
1217
|
+
end
|
1218
|
+
else
|
1219
|
+
@logger.warn('unexpected client connection close and idle terminated.')
|
1220
|
+
res << "#{tag} BAD unexpected client connection close\r\n"
|
1221
|
+
end
|
1222
|
+
yield(res)
|
1223
|
+
end
|
1224
|
+
imap_command_selected :idle, exclusive: nil
|
1225
|
+
end
|
1226
|
+
|
1227
|
+
def Decoder.encode_delivery_target_mailbox(username, mbox_name)
|
1228
|
+
"b64user-mbox #{Protocol.encode_base64(username)} #{mbox_name}"
|
1229
|
+
end
|
1230
|
+
|
1231
|
+
def Decoder.decode_delivery_target_mailbox(encoded_mbox_name)
|
1232
|
+
encode_type, base64_username, mbox_name = encoded_mbox_name.split(' ', 3)
|
1233
|
+
if (encode_type != 'b64user-mbox') then
|
1234
|
+
raise SyntaxError, "unknown mailbox encode type: #{encode_type}"
|
1235
|
+
end
|
1236
|
+
return Protocol.decode_base64(base64_username), mbox_name
|
1237
|
+
end
|
1238
|
+
|
1239
|
+
class MailDeliveryDecoder < AuthenticatedDecoder
|
1240
|
+
def initialize(mail_store_pool, auth, logger,
|
1241
|
+
write_lock_timeout_seconds: ReadWriteLock::DEFAULT_TIMEOUT_SECONDS,
|
1242
|
+
cleanup_write_lock_timeout_seconds: 1,
|
1243
|
+
**mailbox_decoder_optional)
|
1244
|
+
super(auth, logger)
|
1245
|
+
@mail_store_pool = mail_store_pool
|
1246
|
+
@auth = auth
|
1247
|
+
@write_lock_timeout_seconds = write_lock_timeout_seconds
|
1248
|
+
@cleanup_write_lock_timeout_seconds = cleanup_write_lock_timeout_seconds
|
1249
|
+
@mailbox_decoder_optional = mailbox_decoder_optional
|
1250
|
+
@last_user_cache_key_username = nil
|
1251
|
+
@last_user_cache_value_mail_store_holder = nil
|
1252
|
+
end
|
1253
|
+
|
1254
|
+
def user_mail_store_cached?(username)
|
1255
|
+
@last_user_cache_key_username == username
|
1256
|
+
end
|
1257
|
+
private :user_mail_store_cached?
|
1258
|
+
|
1259
|
+
def fetch_user_mail_store_holder(username)
|
1260
|
+
unless (user_mail_store_cached? username) then
|
1261
|
+
release_user_mail_store_holder
|
1262
|
+
@last_user_cache_value_mail_store_holder = yield
|
1263
|
+
@last_user_cache_key_username = username
|
1264
|
+
end
|
1265
|
+
@last_user_cache_value_mail_store_holder
|
1266
|
+
end
|
1267
|
+
private :fetch_user_mail_store_holder
|
1268
|
+
|
1269
|
+
def release_user_mail_store_holder
|
1270
|
+
if (@last_user_cache_value_mail_store_holder) then
|
1271
|
+
mail_store_holder = @last_user_cache_value_mail_store_holder
|
1272
|
+
@last_user_cache_key_username = nil
|
1273
|
+
@last_user_cache_value_mail_store_holder = nil
|
1274
|
+
ReadWriteLock.write_lock_timeout_detach(@cleanup_write_lock_timeout_seconds, @write_lock_timeout_seconds, logger: @logger) {|timeout_seconds|
|
1275
|
+
mail_store_holder.return_pool(timeout_seconds: timeout_seconds) {
|
1276
|
+
@logger.info("close cached mail store to deliver message: #{mail_store_holder.unique_user_id}")
|
1277
|
+
}
|
1278
|
+
}
|
1279
|
+
end
|
1280
|
+
end
|
1281
|
+
private :release_user_mail_store_holder
|
1282
|
+
|
1283
|
+
def auth?
|
1284
|
+
@mail_store_pool != nil
|
1285
|
+
end
|
1286
|
+
|
1287
|
+
def selected?
|
1288
|
+
false
|
1289
|
+
end
|
1290
|
+
|
1291
|
+
def cleanup
|
1292
|
+
release_user_mail_store_holder
|
1293
|
+
@mail_store_pool = nil unless @mail_store_pool.nil?
|
1294
|
+
@auth = nil unless @auth.nil?
|
1295
|
+
nil
|
1296
|
+
end
|
1297
|
+
|
1298
|
+
def logout(tag)
|
1299
|
+
cleanup
|
1300
|
+
res = []
|
1301
|
+
res << "* BYE server logout\r\n"
|
1302
|
+
res << "#{tag} OK LOGOUT completed\r\n"
|
1303
|
+
yield(res)
|
1304
|
+
end
|
1305
|
+
imap_command :logout
|
1306
|
+
|
1307
|
+
alias standard_capability _capability
|
1308
|
+
private :standard_capability
|
1309
|
+
|
1310
|
+
def capability(tag)
|
1311
|
+
standard_capability(tag) {|res|
|
1312
|
+
yield res.map{|line|
|
1313
|
+
if (line.start_with? '* CAPABILITY ') then
|
1314
|
+
line.strip + " X-RIMS-MAIL-DELIVERY-USER\r\n"
|
1315
|
+
else
|
1316
|
+
line
|
1317
|
+
end
|
1318
|
+
}
|
1319
|
+
}
|
1320
|
+
end
|
1321
|
+
imap_command :capability
|
1322
|
+
|
1323
|
+
def not_allowed_command_response(tag)
|
1324
|
+
[ "#{tag} NO not allowed command on mail delivery user\r\n" ]
|
1325
|
+
end
|
1326
|
+
private :not_allowed_command_response
|
1327
|
+
|
1328
|
+
def select(tag, mbox_name)
|
1329
|
+
yield(not_allowed_command_response(tag))
|
1330
|
+
end
|
1331
|
+
imap_command :select
|
1332
|
+
|
1333
|
+
def examine(tag, mbox_name)
|
1334
|
+
yield(not_allowed_command_response(tag))
|
1335
|
+
end
|
1336
|
+
imap_command :examine
|
1337
|
+
|
1338
|
+
def create(tag, mbox_name)
|
1339
|
+
yield(not_allowed_command_response(tag))
|
1340
|
+
end
|
1341
|
+
imap_command :create
|
1342
|
+
|
1343
|
+
def delete(tag, mbox_name)
|
1344
|
+
yield(not_allowed_command_response(tag))
|
1345
|
+
end
|
1346
|
+
imap_command :delete
|
1347
|
+
|
1348
|
+
def rename(tag, src_name, dst_name)
|
1349
|
+
yield(not_allowed_command_response(tag))
|
1350
|
+
end
|
1351
|
+
imap_command :rename
|
1352
|
+
|
1353
|
+
def subscribe(tag, mbox_name)
|
1354
|
+
yield(not_allowed_command_response(tag))
|
1355
|
+
end
|
1356
|
+
imap_command :subscribe
|
1357
|
+
|
1358
|
+
def unsubscribe(tag, mbox_name)
|
1359
|
+
yield(not_allowed_command_response(tag))
|
1360
|
+
end
|
1361
|
+
imap_command :unsubscribe
|
1362
|
+
|
1363
|
+
def list(tag, ref_name, mbox_name)
|
1364
|
+
yield(not_allowed_command_response(tag))
|
1365
|
+
end
|
1366
|
+
imap_command :list
|
1367
|
+
|
1368
|
+
def lsub(tag, ref_name, mbox_name)
|
1369
|
+
yield(not_allowed_command_response(tag))
|
1370
|
+
end
|
1371
|
+
imap_command :lsub
|
1372
|
+
|
1373
|
+
def status(tag, mbox_name, data_item_group)
|
1374
|
+
yield(not_allowed_command_response(tag))
|
1375
|
+
end
|
1376
|
+
imap_command :status
|
1377
|
+
|
1378
|
+
def deliver_to_user(tag, username, mbox_name, opt_args, msg_text, mail_store_holder, res)
|
1379
|
+
user_decoder = UserMailboxDecoder.new(self, mail_store_holder, @auth, @logger,
|
1380
|
+
write_lock_timeout_seconds: @write_lock_timeout_seconds,
|
1381
|
+
cleanup_write_lock_timeout_seconds: @cleanup_write_lock_timeout_seconds,
|
1382
|
+
**@mailbox_decoder_optional)
|
1383
|
+
user_decoder.append(tag, mbox_name, *opt_args, msg_text) {|append_response|
|
1384
|
+
if (append_response.last.split(' ', 3)[1] == 'OK') then
|
1385
|
+
@logger.info("message delivery: successed to deliver #{msg_text.bytesize} octets message.")
|
1386
|
+
else
|
1387
|
+
@logger.info("message delivery: failed to deliver message.")
|
1388
|
+
end
|
1389
|
+
for response_data in append_response
|
1390
|
+
res << response_data
|
1391
|
+
end
|
1392
|
+
}
|
1393
|
+
end
|
1394
|
+
private :deliver_to_user
|
1395
|
+
|
1396
|
+
def append(tag, encoded_mbox_name, *opt_args, msg_text)
|
1397
|
+
username, mbox_name = self.class.decode_delivery_target_mailbox(encoded_mbox_name)
|
1398
|
+
@logger.info("message delivery: user #{username}, mailbox #{mbox_name}")
|
1399
|
+
|
1400
|
+
if (@auth.user? username) then
|
1401
|
+
if (user_mail_store_cached? username) then
|
1402
|
+
res = []
|
1403
|
+
mail_store_holder = fetch_user_mail_store_holder(username)
|
1404
|
+
deliver_to_user(tag, username, mbox_name, opt_args, msg_text, mail_store_holder, res)
|
1405
|
+
else
|
1406
|
+
res = Enumerator.new{|stream_res|
|
1407
|
+
mail_store_holder = fetch_user_mail_store_holder(username) {
|
1408
|
+
self.class.fetch_mail_store_holder_and_on_demand_recovery(@mail_store_pool, username,
|
1409
|
+
write_lock_timeout_seconds: @write_lock_timeout_seconds,
|
1410
|
+
logger: @logger) {|msg| stream_res << msg }
|
1411
|
+
}
|
1412
|
+
deliver_to_user(tag, username, mbox_name, opt_args, msg_text, mail_store_holder, stream_res)
|
1413
|
+
}
|
1414
|
+
end
|
1415
|
+
yield(res)
|
1416
|
+
else
|
1417
|
+
@logger.info('message delivery: not found a user.')
|
1418
|
+
yield([ "#{tag} NO not found a user and couldn't deliver a message to the user's mailbox\r\n" ])
|
1419
|
+
end
|
1420
|
+
end
|
1421
|
+
imap_command :append
|
1422
|
+
|
1423
|
+
def check(tag)
|
1424
|
+
yield(not_allowed_command_response(tag))
|
1425
|
+
end
|
1426
|
+
imap_command :check
|
1427
|
+
|
1428
|
+
def close(tag)
|
1429
|
+
yield(not_allowed_command_response(tag))
|
1430
|
+
end
|
1431
|
+
imap_command :close
|
1432
|
+
|
1433
|
+
def expunge(tag)
|
1434
|
+
yield(not_allowed_command_response(tag))
|
1435
|
+
end
|
1436
|
+
imap_command :expunge
|
1437
|
+
|
1438
|
+
def search(tag, *cond_args, uid: false)
|
1439
|
+
yield(not_allowed_command_response(tag))
|
1440
|
+
end
|
1441
|
+
imap_command :search
|
1442
|
+
|
1443
|
+
def fetch(tag, msg_set, data_item_group, uid: false)
|
1444
|
+
yield(not_allowed_command_response(tag))
|
1445
|
+
end
|
1446
|
+
imap_command :fetch
|
1447
|
+
|
1448
|
+
def store(tag, msg_set, data_item_name, data_item_value, uid: false)
|
1449
|
+
yield(not_allowed_command_response(tag))
|
1450
|
+
end
|
1451
|
+
imap_command :store
|
1452
|
+
|
1453
|
+
def copy(tag, msg_set, mbox_name, uid: false)
|
1454
|
+
yield(not_allowed_command_response(tag))
|
1455
|
+
end
|
1456
|
+
imap_command :copy
|
1457
|
+
|
1458
|
+
def idle(tag, client_input_stream, server_output_stream)
|
1459
|
+
yield(not_allowed_command_response(tag))
|
1460
|
+
end
|
1461
|
+
imap_command :idle
|
1462
|
+
end
|
1463
|
+
end
|
1464
|
+
end
|
1465
|
+
|
1466
|
+
# Local Variables:
|
1467
|
+
# mode: Ruby
|
1468
|
+
# indent-tabs-mode: nil
|
1469
|
+
# End:
|