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/daemon.rb
ADDED
@@ -0,0 +1,338 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module RIMS
|
7
|
+
class Daemon
|
8
|
+
class ExclusiveStatusFile
|
9
|
+
def initialize(filename)
|
10
|
+
@filename = filename
|
11
|
+
@file = nil
|
12
|
+
@is_locked = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def open
|
16
|
+
if (block_given?) then
|
17
|
+
open
|
18
|
+
begin
|
19
|
+
r = yield
|
20
|
+
ensure
|
21
|
+
close
|
22
|
+
end
|
23
|
+
return r
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
@file = File.open(@filename, File::WRONLY | File::CREAT, 0640)
|
28
|
+
rescue SystemCallError
|
29
|
+
@fiile = File.open(@filename, File::WRONLY)
|
30
|
+
end
|
31
|
+
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def close
|
36
|
+
@file.close
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def locked?
|
41
|
+
@is_locked
|
42
|
+
end
|
43
|
+
|
44
|
+
def should_be_locked
|
45
|
+
unless (locked?) then
|
46
|
+
raise "not locked: #{@filename}"
|
47
|
+
end
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def should_not_be_locked
|
52
|
+
if (locked?) then
|
53
|
+
raise "already locked: #{@filename}"
|
54
|
+
end
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def lock
|
59
|
+
should_not_be_locked
|
60
|
+
unless (@file.flock(File::LOCK_EX | File::LOCK_NB)) then
|
61
|
+
raise "locked by another process: #{@filename}"
|
62
|
+
end
|
63
|
+
@is_locked = true
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def unlock
|
68
|
+
should_be_locked
|
69
|
+
@file.flock(File::LOCK_UN)
|
70
|
+
@is_locked = false
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
def synchronize
|
75
|
+
lock
|
76
|
+
begin
|
77
|
+
yield
|
78
|
+
ensure
|
79
|
+
unlock
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def write(text)
|
84
|
+
should_be_locked
|
85
|
+
|
86
|
+
@file.truncate(0)
|
87
|
+
@file.syswrite(text)
|
88
|
+
|
89
|
+
self
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class ReadableStatusFile
|
94
|
+
def initialize(filename)
|
95
|
+
@filename = filename
|
96
|
+
@file = nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def open
|
100
|
+
if (block_given?) then
|
101
|
+
open
|
102
|
+
begin
|
103
|
+
r = yield
|
104
|
+
ensure
|
105
|
+
close
|
106
|
+
end
|
107
|
+
return r
|
108
|
+
end
|
109
|
+
|
110
|
+
@file = File.open(@filename, File::RDONLY)
|
111
|
+
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
def close
|
116
|
+
@file.close
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
def locked?
|
121
|
+
if (@file.flock(File::LOCK_EX | File::LOCK_NB)) then
|
122
|
+
@file.flock(File::LOCK_UN)
|
123
|
+
false
|
124
|
+
else
|
125
|
+
true
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def should_be_locked
|
130
|
+
unless (locked?) then
|
131
|
+
raise "not locked: #{@filename}"
|
132
|
+
end
|
133
|
+
self
|
134
|
+
end
|
135
|
+
|
136
|
+
def read
|
137
|
+
should_be_locked
|
138
|
+
@file.seek(0)
|
139
|
+
@file.read
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.new_status_file(filename, exclusive: false)
|
144
|
+
if (exclusive) then
|
145
|
+
ExclusiveStatusFile.new(filename)
|
146
|
+
else
|
147
|
+
ReadableStatusFile.new(filename)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.make_stat_file_path(base_dir)
|
152
|
+
File.join(base_dir, 'status')
|
153
|
+
end
|
154
|
+
|
155
|
+
RELOAD_SIGNAL_LIST = %w[ HUP ]
|
156
|
+
RESTART_SIGNAL_LIST = %w[ USR1 ]
|
157
|
+
STOP_SIGNAL_LIST = %w[ TERM INT ]
|
158
|
+
|
159
|
+
RELOAD_SIGNAL = RELOAD_SIGNAL_LIST[0]
|
160
|
+
RESTART_SIGNAL = RESTART_SIGNAL_LIST[0]
|
161
|
+
STOP_SIGNAL = STOP_SIGNAL_LIST[0]
|
162
|
+
|
163
|
+
SERVER_RESTART_INTERVAL_SECONDS = 5
|
164
|
+
|
165
|
+
class ChildProcess
|
166
|
+
# return self if child process has been existed.
|
167
|
+
# return nil if no child process.
|
168
|
+
def self.cleanup_terminated_process(logger)
|
169
|
+
begin
|
170
|
+
while (pid = Process.waitpid(-1))
|
171
|
+
if ($?.exitstatus != 0) then
|
172
|
+
logger.warn("aborted child process: #{pid} (#{$?.exitstatus})")
|
173
|
+
end
|
174
|
+
yield(pid) if block_given?
|
175
|
+
end
|
176
|
+
rescue Errno::ECHILD
|
177
|
+
return
|
178
|
+
end
|
179
|
+
|
180
|
+
self
|
181
|
+
end
|
182
|
+
|
183
|
+
def initialize(logger=Logger.new(STDOUT))
|
184
|
+
@logger = logger
|
185
|
+
@pid = run{ yield }
|
186
|
+
end
|
187
|
+
|
188
|
+
attr_reader :pid
|
189
|
+
|
190
|
+
def run
|
191
|
+
begin
|
192
|
+
pipe_in, pipe_out = IO.pipe
|
193
|
+
pid = Process.fork{
|
194
|
+
@logger.close
|
195
|
+
pipe_in.close
|
196
|
+
|
197
|
+
status_code = catch(:rims_daemon_child_process_stop) {
|
198
|
+
for sig_name in RELOAD_SIGNAL_LIST + RESTART_SIGNAL_LIST
|
199
|
+
Signal.trap(sig_name, :DEFAULT)
|
200
|
+
end
|
201
|
+
for sig_name in STOP_SIGNAL_LIST
|
202
|
+
Signal.trap(sig_name) { throw(:rims_daemon_child_process_stop, 0) }
|
203
|
+
end
|
204
|
+
|
205
|
+
pipe_out.puts("child process (pid: #{$$}) is ready to go.")
|
206
|
+
pipe_out.close
|
207
|
+
|
208
|
+
yield
|
209
|
+
}
|
210
|
+
exit!(status_code)
|
211
|
+
}
|
212
|
+
rescue
|
213
|
+
@logger.error("failed to fork new child process: #{$!}")
|
214
|
+
return
|
215
|
+
end
|
216
|
+
|
217
|
+
begin
|
218
|
+
pipe_out.close
|
219
|
+
s = pipe_in.gets
|
220
|
+
@logger.info("[child process message] #{s}") if $DEBUG
|
221
|
+
pipe_in.close
|
222
|
+
rescue
|
223
|
+
@logger.error("failed to start new child process: #{$!}")
|
224
|
+
begin
|
225
|
+
Process.kill(STOP_SIGNAL, pid)
|
226
|
+
rescue SystemCallError
|
227
|
+
@logger.warn("failed to kill abnormal child process: #{$!}")
|
228
|
+
end
|
229
|
+
return
|
230
|
+
end
|
231
|
+
|
232
|
+
pid
|
233
|
+
end
|
234
|
+
private :run
|
235
|
+
|
236
|
+
def forked?
|
237
|
+
@pid != nil
|
238
|
+
end
|
239
|
+
|
240
|
+
def terminate
|
241
|
+
begin
|
242
|
+
Process.kill(STOP_SIGNAL, @pid)
|
243
|
+
rescue SystemCallError
|
244
|
+
@logger.warn("failed to terminate child process: #{@pid}")
|
245
|
+
end
|
246
|
+
|
247
|
+
nil
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def initialize(stat_file_path, logger=Logger.new(STDOUT), server_options: [])
|
252
|
+
@stat_file = self.class.new_status_file(stat_file_path, exclusive: true)
|
253
|
+
@logger = logger
|
254
|
+
@server_options = server_options
|
255
|
+
@server_running = true
|
256
|
+
@server_process = nil
|
257
|
+
end
|
258
|
+
|
259
|
+
def new_server_process
|
260
|
+
ChildProcess.new(@logger) { Cmd.run_cmd(%w[ server ] + @server_options) }
|
261
|
+
end
|
262
|
+
private :new_server_process
|
263
|
+
|
264
|
+
def run
|
265
|
+
@stat_file.open{
|
266
|
+
@stat_file.synchronize{
|
267
|
+
@stat_file.write({ 'pid' => $$ }.to_yaml)
|
268
|
+
begin
|
269
|
+
@logger.info('start daemon.')
|
270
|
+
loop do
|
271
|
+
break unless @server_running
|
272
|
+
|
273
|
+
unless (@server_process && @server_process.forked?) then
|
274
|
+
start_time = Time.now
|
275
|
+
@server_process = new_server_process
|
276
|
+
@logger.info("run server process: #{@server_process.pid}")
|
277
|
+
end
|
278
|
+
|
279
|
+
break unless @server_running
|
280
|
+
|
281
|
+
ChildProcess.cleanup_terminated_process(@logger) do |pid|
|
282
|
+
if (@server_process.pid == pid) then
|
283
|
+
@server_process = nil
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
break unless @server_running
|
288
|
+
|
289
|
+
elapsed_seconds = Time.now - start_time
|
290
|
+
the_rest_in_interval_seconds = SERVER_RESTART_INTERVAL_SECONDS - elapsed_seconds
|
291
|
+
sleep(the_rest_in_interval_seconds) if (the_rest_in_interval_seconds > 0)
|
292
|
+
end
|
293
|
+
ensure
|
294
|
+
if (@server_process && @server_process.forked?) then
|
295
|
+
@server_process.terminate
|
296
|
+
ChildProcess.cleanup_terminated_process(@logger)
|
297
|
+
end
|
298
|
+
@logger.info('stop daemon.')
|
299
|
+
end
|
300
|
+
}
|
301
|
+
}
|
302
|
+
|
303
|
+
self
|
304
|
+
end
|
305
|
+
|
306
|
+
# signal trap hook.
|
307
|
+
# this method is not true reload.
|
308
|
+
def reload_server
|
309
|
+
restart_server
|
310
|
+
end
|
311
|
+
|
312
|
+
# signal trap hook.
|
313
|
+
def restart_server
|
314
|
+
@stat_file.should_be_locked
|
315
|
+
if (@server_process && @server_process.forked?) then
|
316
|
+
@server_process.terminate
|
317
|
+
end
|
318
|
+
|
319
|
+
self
|
320
|
+
end
|
321
|
+
|
322
|
+
# signal trap hook.
|
323
|
+
def stop_server
|
324
|
+
@stat_file.should_be_locked
|
325
|
+
@server_running = false
|
326
|
+
if (@server_process && @server_process.forked?) then
|
327
|
+
@server_process.terminate
|
328
|
+
end
|
329
|
+
|
330
|
+
self
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# Local Variables:
|
336
|
+
# mode: Ruby
|
337
|
+
# indent-tabs-mode: nil
|
338
|
+
# End:
|
data/lib/rims/db.rb
ADDED
@@ -0,0 +1,793 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
module RIMS
|
7
|
+
module DB
|
8
|
+
class Core
|
9
|
+
def initialize(kvs)
|
10
|
+
@kvs = kvs
|
11
|
+
end
|
12
|
+
|
13
|
+
def sync
|
14
|
+
@kvs.sync
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def close
|
19
|
+
@kvs.close
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def destroy
|
24
|
+
@kvs.destroy
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_read_all # :yields: read_error
|
29
|
+
last_error = nil
|
30
|
+
@kvs.each_key do |key|
|
31
|
+
begin
|
32
|
+
@kvs[key]
|
33
|
+
rescue
|
34
|
+
last_error = $!
|
35
|
+
yield($!)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
if (last_error) then
|
40
|
+
raise last_error
|
41
|
+
end
|
42
|
+
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_str(key, default_value: nil)
|
47
|
+
@kvs[key] || default_value
|
48
|
+
end
|
49
|
+
private :get_str
|
50
|
+
|
51
|
+
def put_str(key, str)
|
52
|
+
@kvs[key] = str
|
53
|
+
self
|
54
|
+
end
|
55
|
+
private :put_str
|
56
|
+
|
57
|
+
def get_str_set(key)
|
58
|
+
if (s = @kvs[key]) then
|
59
|
+
s.split(',', -1).to_set
|
60
|
+
else
|
61
|
+
[].to_set
|
62
|
+
end
|
63
|
+
end
|
64
|
+
private :get_str_set
|
65
|
+
|
66
|
+
def put_str_set(key, str_set)
|
67
|
+
@kvs[key] = str_set.to_a.join(',')
|
68
|
+
self
|
69
|
+
end
|
70
|
+
private :put_str_set
|
71
|
+
|
72
|
+
def get_num(key, default_value: 0)
|
73
|
+
if (s = @kvs[key]) then
|
74
|
+
s.to_i
|
75
|
+
else
|
76
|
+
default_value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
private :get_num
|
80
|
+
|
81
|
+
def put_num(key, num)
|
82
|
+
@kvs[key] = num.to_s
|
83
|
+
self
|
84
|
+
end
|
85
|
+
private :put_num
|
86
|
+
|
87
|
+
def num_succ!(key, default_value: 0)
|
88
|
+
n = get_num(key, default_value: default_value)
|
89
|
+
put_num(key, n + 1)
|
90
|
+
n
|
91
|
+
end
|
92
|
+
private :num_succ!
|
93
|
+
|
94
|
+
def num_increment(key)
|
95
|
+
n = get_num(key)
|
96
|
+
put_num(key, n + 1)
|
97
|
+
self
|
98
|
+
end
|
99
|
+
private :num_increment
|
100
|
+
|
101
|
+
def num_decrement(key)
|
102
|
+
n = get_num(key)
|
103
|
+
put_num(key, n - 1)
|
104
|
+
self
|
105
|
+
end
|
106
|
+
private :num_decrement
|
107
|
+
|
108
|
+
def get_num_set(key)
|
109
|
+
if (s = @kvs[key]) then
|
110
|
+
s.split(',', -1).map{|n| n.to_i }.to_set
|
111
|
+
else
|
112
|
+
[].to_set
|
113
|
+
end
|
114
|
+
end
|
115
|
+
private :get_num_set
|
116
|
+
|
117
|
+
def put_num_set(key, num_set)
|
118
|
+
@kvs[key] = num_set.to_a.join(',')
|
119
|
+
self
|
120
|
+
end
|
121
|
+
private :put_num_set
|
122
|
+
|
123
|
+
def get_obj(key, default_value: nil)
|
124
|
+
if (s = @kvs[key]) then
|
125
|
+
Marshal.load(s)
|
126
|
+
else
|
127
|
+
default_value
|
128
|
+
end
|
129
|
+
end
|
130
|
+
private :get_obj
|
131
|
+
|
132
|
+
def put_obj(key, value)
|
133
|
+
@kvs[key] = Marshal.dump(value)
|
134
|
+
self
|
135
|
+
end
|
136
|
+
private :put_obj
|
137
|
+
end
|
138
|
+
|
139
|
+
class Meta < Core
|
140
|
+
def dirty?
|
141
|
+
@kvs.key? 'dirty'
|
142
|
+
end
|
143
|
+
|
144
|
+
def dirty=(dirty_flag)
|
145
|
+
if (dirty_flag) then
|
146
|
+
put_str('dirty', '')
|
147
|
+
else
|
148
|
+
@kvs.delete('dirty')
|
149
|
+
end
|
150
|
+
@kvs.sync
|
151
|
+
|
152
|
+
dirty_flag
|
153
|
+
end
|
154
|
+
|
155
|
+
def cnum
|
156
|
+
get_num('cnum')
|
157
|
+
end
|
158
|
+
|
159
|
+
def cnum_succ!
|
160
|
+
num_succ!('cnum')
|
161
|
+
end
|
162
|
+
|
163
|
+
def msg_id
|
164
|
+
get_num('msg_id')
|
165
|
+
end
|
166
|
+
|
167
|
+
def msg_id_succ!
|
168
|
+
num_succ!('msg_id')
|
169
|
+
end
|
170
|
+
|
171
|
+
def uidvalidity
|
172
|
+
get_num('uidvalidity', default_value: 1)
|
173
|
+
end
|
174
|
+
|
175
|
+
def uidvalidity_succ!
|
176
|
+
num_succ!('uidvalidity', default_value: 1)
|
177
|
+
end
|
178
|
+
|
179
|
+
def add_mbox(name, mbox_id: nil)
|
180
|
+
if (@kvs.key? "mbox_name2id-#{name}") then
|
181
|
+
raise "duplicated mailbox name: #{name}."
|
182
|
+
end
|
183
|
+
|
184
|
+
if (mbox_id) then
|
185
|
+
if (@kvs.key? "mbox_id2name-#{mbox_id}") then
|
186
|
+
raise "duplicated mailbox id: #{mbox_id}"
|
187
|
+
end
|
188
|
+
if (uidvalidity <= mbox_id) then
|
189
|
+
put_num('uidvalidity', mbox_id + 1)
|
190
|
+
end
|
191
|
+
else
|
192
|
+
mbox_id = uidvalidity_succ!
|
193
|
+
end
|
194
|
+
|
195
|
+
mbox_set = get_num_set('mbox_set')
|
196
|
+
if (mbox_set.include? mbox_id) then
|
197
|
+
raise "internal error: duplicated mailbox id: #{mbox_id}"
|
198
|
+
end
|
199
|
+
mbox_set << mbox_id
|
200
|
+
put_num_set('mbox_set', mbox_set)
|
201
|
+
|
202
|
+
put_str("mbox_id2name-#{mbox_id}", name)
|
203
|
+
put_num("mbox_name2id-#{name}", mbox_id)
|
204
|
+
|
205
|
+
mbox_id
|
206
|
+
end
|
207
|
+
|
208
|
+
def del_mbox(mbox_id)
|
209
|
+
mbox_set = get_num_set('mbox_set')
|
210
|
+
if (mbox_set.include? mbox_id) then
|
211
|
+
mbox_set.delete(mbox_id)
|
212
|
+
put_num_set('mbox_set', mbox_set)
|
213
|
+
name = mbox_name(mbox_id)
|
214
|
+
@kvs.delete("mbox_id2name-#{mbox_id}") or raise "not found a mailbox name for id: #{mbox_id}"
|
215
|
+
@kvs.delete("mbox_name2id-#{name}") or raise "not found a mailbox id for name: #{name}"
|
216
|
+
@kvs.delete("mbox_id2uid-#{mbox_id}")
|
217
|
+
@kvs.delete("mbox_id2msgnum-#{mbox_id}")
|
218
|
+
self
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def rename_mbox(mbox_id, new_name)
|
223
|
+
old_name = get_str("mbox_id2name-#{mbox_id}") or raise "not found a mailbox name for id: #{mbox_id}"
|
224
|
+
if (new_name == old_name) then
|
225
|
+
return
|
226
|
+
end
|
227
|
+
if (@kvs.key? "mbox_name2id-#{new_name}") then
|
228
|
+
raise "duplicated mailbox name: #{new_name}"
|
229
|
+
end
|
230
|
+
@kvs.delete("mbox_name2id-#{old_name}") or raise "not found a mailbox old name for id: #{mbox_id}"
|
231
|
+
put_str("mbox_id2name-#{mbox_id}", new_name)
|
232
|
+
put_num("mbox_name2id-#{new_name}", mbox_id)
|
233
|
+
self
|
234
|
+
end
|
235
|
+
|
236
|
+
def each_mbox_id
|
237
|
+
return enum_for(:each_mbox_id) unless block_given?
|
238
|
+
mbox_set = get_num_set('mbox_set')
|
239
|
+
for mbox_id in mbox_set
|
240
|
+
yield(mbox_id)
|
241
|
+
end
|
242
|
+
self
|
243
|
+
end
|
244
|
+
|
245
|
+
def mbox_name(mbox_id)
|
246
|
+
get_str("mbox_id2name-#{mbox_id}", default_value: nil)
|
247
|
+
end
|
248
|
+
|
249
|
+
def mbox_id(name)
|
250
|
+
get_num("mbox_name2id-#{name}", default_value: nil)
|
251
|
+
end
|
252
|
+
|
253
|
+
def mbox_uid(mbox_id)
|
254
|
+
mbox_name(mbox_id) or raise "not found a mailbox for id: #{mbox_id}"
|
255
|
+
get_num("mbox_id2uid-#{mbox_id}", default_value: 1)
|
256
|
+
end
|
257
|
+
|
258
|
+
def mbox_uid_succ!(mbox_id)
|
259
|
+
mbox_name(mbox_id) or raise "not found a mailbox for id: #{mbox_id}"
|
260
|
+
num_succ!("mbox_id2uid-#{mbox_id}", default_value: 1)
|
261
|
+
end
|
262
|
+
|
263
|
+
def mbox_msg_num(mbox_id)
|
264
|
+
mbox_name(mbox_id) or raise "not found a mailbox for id: #{mbox_id}"
|
265
|
+
get_num("mbox_id2msgnum-#{mbox_id}")
|
266
|
+
end
|
267
|
+
|
268
|
+
def mbox_msg_num_increment(mbox_id)
|
269
|
+
mbox_name(mbox_id) or raise "not found a mailbox for id: #{mbox_id}"
|
270
|
+
num_increment("mbox_id2msgnum-#{mbox_id}")
|
271
|
+
self
|
272
|
+
end
|
273
|
+
|
274
|
+
def mbox_msg_num_decrement(mbox_id)
|
275
|
+
mbox_name(mbox_id) or raise "not found a mailbox for id: #{mbox_id}"
|
276
|
+
num_decrement("mbox_id2msgnum-#{mbox_id}")
|
277
|
+
self
|
278
|
+
end
|
279
|
+
|
280
|
+
def mbox_flag_num(mbox_id, name)
|
281
|
+
mbox_name(mbox_id) or raise "not found a mailbox for id: #{mbox_id}"
|
282
|
+
get_num("mbox_id2flagnum-#{mbox_id}-#{name}")
|
283
|
+
end
|
284
|
+
|
285
|
+
def mbox_flag_num_increment(mbox_id, name)
|
286
|
+
mbox_name(mbox_id) or raise "not found a mailbox for id: #{mbox_id}"
|
287
|
+
num_increment("mbox_id2flagnum-#{mbox_id}-#{name}")
|
288
|
+
self
|
289
|
+
end
|
290
|
+
|
291
|
+
def mbox_flag_num_decrement(mbox_id, name)
|
292
|
+
mbox_name(mbox_id) or raise "not found a mailbox for id: #{mbox_id}"
|
293
|
+
num_decrement("mbox_id2flagnum-#{mbox_id}-#{name}")
|
294
|
+
self
|
295
|
+
end
|
296
|
+
|
297
|
+
def clear_mbox_flag_num(mbox_id, name)
|
298
|
+
mbox_name(mbox_id) or raise "not found a mailbox for id: #{mbox_id}"
|
299
|
+
if (@kvs.delete("mbox_id2flagnum-#{mbox_id}-#{name}")) then
|
300
|
+
self
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def msg_date(msg_id)
|
305
|
+
get_obj("msg_id2date-#{msg_id}") or raise "not found a message date for internal id: #{msg_id}"
|
306
|
+
end
|
307
|
+
|
308
|
+
def set_msg_date(msg_id, date)
|
309
|
+
put_obj("msg_id2date-#{msg_id}", date)
|
310
|
+
self
|
311
|
+
end
|
312
|
+
|
313
|
+
def clear_msg_date(msg_id)
|
314
|
+
if (@kvs.delete("msg_id2date-#{msg_id}")) then
|
315
|
+
self
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def msg_flag(msg_id, name)
|
320
|
+
flag_set = get_str_set("msg_id2flag-#{msg_id}")
|
321
|
+
flag_set.include? name
|
322
|
+
end
|
323
|
+
|
324
|
+
def set_msg_flag(msg_id, name, value)
|
325
|
+
flag_set = get_str_set("msg_id2flag-#{msg_id}")
|
326
|
+
if (value) then
|
327
|
+
unless (flag_set.include? name) then
|
328
|
+
mbox_uid_map = msg_mbox_uid_mapping(msg_id)
|
329
|
+
for mbox_id, uid_set in mbox_uid_map
|
330
|
+
uid_set.length.times do
|
331
|
+
mbox_flag_num_increment(mbox_id, name)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
flag_set.add(name)
|
336
|
+
else
|
337
|
+
if (flag_set.include? name) then
|
338
|
+
mbox_uid_map = msg_mbox_uid_mapping(msg_id)
|
339
|
+
for mbox_id, uid_set in mbox_uid_map
|
340
|
+
uid_set.length.times do
|
341
|
+
mbox_flag_num_decrement(mbox_id, name)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
flag_set.delete(name)
|
346
|
+
end
|
347
|
+
put_str_set("msg_id2flag-#{msg_id}", flag_set)
|
348
|
+
self
|
349
|
+
end
|
350
|
+
|
351
|
+
def clear_msg_flag(msg_id)
|
352
|
+
if (@kvs.delete("msg_id2flag-#{msg_id}")) then
|
353
|
+
self
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def msg_mbox_uid_mapping(msg_id)
|
358
|
+
get_obj("msg_id2mbox-#{msg_id}", default_value: {})
|
359
|
+
end
|
360
|
+
|
361
|
+
def add_msg_mbox_uid(msg_id, mbox_id)
|
362
|
+
uid = mbox_uid_succ!(mbox_id)
|
363
|
+
mbox_uid_map = msg_mbox_uid_mapping(msg_id)
|
364
|
+
if (mbox_uid_map.key? mbox_id) then
|
365
|
+
msg_uid_set = mbox_uid_map[mbox_id]
|
366
|
+
else
|
367
|
+
msg_uid_set = mbox_uid_map[mbox_id] = [].to_set
|
368
|
+
end
|
369
|
+
if (msg_uid_set.include? uid) then
|
370
|
+
raise "duplicated uid(#{uid}) in mailbox id(#{mbox_id} on message id(#{msg_id}))"
|
371
|
+
end
|
372
|
+
mbox_uid_map[mbox_id] << uid
|
373
|
+
put_obj("msg_id2mbox-#{msg_id}", mbox_uid_map)
|
374
|
+
|
375
|
+
mbox_msg_num_increment(mbox_id)
|
376
|
+
flag_set = get_str_set("msg_id2flag-#{msg_id}")
|
377
|
+
for name in flag_set
|
378
|
+
mbox_flag_num_increment(mbox_id, name)
|
379
|
+
end
|
380
|
+
|
381
|
+
uid
|
382
|
+
end
|
383
|
+
|
384
|
+
def del_msg_mbox_uid(msg_id, mbox_id, uid)
|
385
|
+
mbox_uid_map = msg_mbox_uid_mapping(msg_id)
|
386
|
+
if (uid_set = mbox_uid_map[mbox_id]) then
|
387
|
+
if (uid_set.include? uid) then
|
388
|
+
uid_set.delete(uid)
|
389
|
+
mbox_uid_map.delete(mbox_id) if uid_set.empty?
|
390
|
+
put_obj("msg_id2mbox-#{msg_id}", mbox_uid_map)
|
391
|
+
|
392
|
+
mbox_msg_num_decrement(mbox_id)
|
393
|
+
flag_set = get_str_set("msg_id2flag-#{msg_id}")
|
394
|
+
for name in flag_set
|
395
|
+
mbox_flag_num_decrement(mbox_id, name)
|
396
|
+
end
|
397
|
+
|
398
|
+
mbox_uid_map
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
def clear_msg_mbox_uid_mapping(msg_id)
|
404
|
+
if (@kvs.delete("msg_id2mbox-#{msg_id}")) then
|
405
|
+
self
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
def recovery_start
|
410
|
+
@lost_found_msg_set = [].to_set
|
411
|
+
@lost_found_mbox_set = [].to_set
|
412
|
+
end
|
413
|
+
|
414
|
+
def recovery_end
|
415
|
+
@lost_found_msg_set = nil
|
416
|
+
@lost_found_mbox_set = nil
|
417
|
+
end
|
418
|
+
|
419
|
+
attr_reader :lost_found_msg_set
|
420
|
+
attr_reader :lost_found_mbox_set
|
421
|
+
|
422
|
+
def get_recover_entry(key, prefix)
|
423
|
+
if (key.start_with? prefix) then
|
424
|
+
entry_key = key[(prefix.length)..-1]
|
425
|
+
entry_key = yield(entry_key) if block_given?
|
426
|
+
entry_key
|
427
|
+
end
|
428
|
+
end
|
429
|
+
private :get_recover_entry
|
430
|
+
|
431
|
+
def recovery_phase1_msg_scan(msg_db, logger: Logger.new(STDOUT))
|
432
|
+
logger.info('recovery phase 1: start.')
|
433
|
+
|
434
|
+
max_msg_id = -1
|
435
|
+
msg_db.each_msg_id do |msg_id|
|
436
|
+
max_msg_id = msg_id if (max_msg_id < msg_id)
|
437
|
+
unless (@kvs.key? "msg_id2mbox-#{msg_id}") then
|
438
|
+
logger.warn("lost+found message: #{msg_id}")
|
439
|
+
@lost_found_msg_set << msg_id
|
440
|
+
end
|
441
|
+
unless (@kvs.key? "msg_id2date-#{msg_id}") then
|
442
|
+
logger.warn("repair internal date: #{msg_id}")
|
443
|
+
set_msg_date(msg_id, Time.now)
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
if (msg_id <= max_msg_id) then
|
448
|
+
next_msg_id = max_msg_id + 1
|
449
|
+
logger.warn("repair msg_id: #{next_msg_id}")
|
450
|
+
put_num('msg_id', next_msg_id)
|
451
|
+
end
|
452
|
+
|
453
|
+
logger.info('recovery phase 1: end.')
|
454
|
+
|
455
|
+
self
|
456
|
+
end
|
457
|
+
|
458
|
+
def recovery_phase2_msg_scan(msg_db, logger: Logger.new(STDOUT))
|
459
|
+
logger.info('recovery phase 2: start.')
|
460
|
+
|
461
|
+
lost_msg_set = [].to_set
|
462
|
+
mbox_set = get_num_set('mbox_set')
|
463
|
+
|
464
|
+
@kvs.each_key do |key|
|
465
|
+
if (msg_id = get_recover_entry(key, 'msg_id2mbox-') {|s| s.to_i }) then
|
466
|
+
if (msg_db.msg_exist? msg_id) then
|
467
|
+
msg_mbox_uid_mapping(msg_id).each_key do |mbox_id|
|
468
|
+
unless (mbox_set.include? mbox_id) then
|
469
|
+
logger.warn("lost+found mailbox: #{mbox_id}")
|
470
|
+
@lost_found_mbox_set << mbox_id
|
471
|
+
end
|
472
|
+
end
|
473
|
+
else
|
474
|
+
lost_msg_set << msg_id
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
for msg_id in lost_msg_set
|
480
|
+
logger.warn("clear lost message: #{msg_id}")
|
481
|
+
clear_msg_date(msg_id)
|
482
|
+
clear_msg_flag(msg_id)
|
483
|
+
clear_msg_mbox_uid_mapping(msg_id)
|
484
|
+
end
|
485
|
+
|
486
|
+
logger.info('recovery phase 2: end.')
|
487
|
+
|
488
|
+
self
|
489
|
+
end
|
490
|
+
|
491
|
+
def make_mbox_repair_name(mbox_id)
|
492
|
+
new_name = "MAILBOX##{mbox_id}"
|
493
|
+
if (mbox_id(new_name)) then
|
494
|
+
new_name << ' (1)'
|
495
|
+
while (mbox_id(new_name))
|
496
|
+
new_name.succ!
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
new_name
|
501
|
+
end
|
502
|
+
private :make_mbox_repair_name
|
503
|
+
|
504
|
+
def recovery_phase3_mbox_scan(logger: Logger.new(STDOUT))
|
505
|
+
logger.info('recovery phase 3: start.')
|
506
|
+
|
507
|
+
mbox_set = get_num_set('mbox_set')
|
508
|
+
|
509
|
+
max_mbox_id = 0
|
510
|
+
for mbox_id in mbox_set
|
511
|
+
max_mbox_id = mbox_id if (mbox_id > max_mbox_id)
|
512
|
+
if (name = mbox_name(mbox_id)) then
|
513
|
+
mbox_id2 = mbox_id(name)
|
514
|
+
unless (mbox_id2 && (mbox_id2 == mbox_id)) then
|
515
|
+
logger.warn("repair mailbox name -> id: #{name.inspect} -> #{mbox_id}")
|
516
|
+
put_num("mbox_name2id-#{name}", mbox_id)
|
517
|
+
end
|
518
|
+
else
|
519
|
+
new_name = make_mbox_repair_name(mbox_id)
|
520
|
+
logger.warn("repair mailbox id name pair: #{mbox_id}, #{new_name.inspect}")
|
521
|
+
put_str("mbox_id2name-#{mbox_id}", new_name)
|
522
|
+
put_num("mbox_name2id-#{new_name}", mbox_id)
|
523
|
+
end
|
524
|
+
end
|
525
|
+
|
526
|
+
if (uidvalidity <= max_mbox_id) then
|
527
|
+
next_uidvalidity = max_mbox_id + 1
|
528
|
+
logger.warn("repair uidvalidity: #{next_uidvalidity}")
|
529
|
+
put_num('uidvalidity', next_uidvalidity)
|
530
|
+
end
|
531
|
+
|
532
|
+
logger.info('recovery phase 3: end.')
|
533
|
+
|
534
|
+
self
|
535
|
+
end
|
536
|
+
|
537
|
+
def recovery_phase4_mbox_scan(logger: Logger.new(STDOUT))
|
538
|
+
logger.info('recovery phase 4: start.')
|
539
|
+
|
540
|
+
mbox_set = get_num_set('mbox_set')
|
541
|
+
|
542
|
+
del_key_list = []
|
543
|
+
@kvs.each_key do |key|
|
544
|
+
if (mbox_id = get_recover_entry(key, 'mbox_id2name-') {|s| s.to_i }) then
|
545
|
+
unless (mbox_set.include? mbox_id) then
|
546
|
+
del_key_list << key
|
547
|
+
end
|
548
|
+
elsif (name = get_recover_entry(key, 'mbox_name2id-')) then
|
549
|
+
unless ((mbox_id = mbox_id(name)) && (mbox_set.include? mbox_id) && (mbox_name(mbox_id) == name)) then
|
550
|
+
del_key_list << key
|
551
|
+
end
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
for key in del_key_list
|
556
|
+
logger.warn("unlinked mailbox entry: #{key}")
|
557
|
+
@kvs.delete(key)
|
558
|
+
end
|
559
|
+
|
560
|
+
logger.info('recovery phase 4: end.')
|
561
|
+
|
562
|
+
self
|
563
|
+
end
|
564
|
+
|
565
|
+
LOST_FOUND_MBOX_NAME = 'lost+found'.freeze
|
566
|
+
|
567
|
+
def recovery_phase5_mbox_repair(logger: Logger.new(STDOUT))
|
568
|
+
logger.info('recovery phase 5: start.')
|
569
|
+
|
570
|
+
for mbox_id in @lost_found_mbox_set
|
571
|
+
logger.warn("repair lost mailbox: #{mbox_id}")
|
572
|
+
add_mbox(make_mbox_repair_name(mbox_id), mbox_id: mbox_id)
|
573
|
+
yield(mbox_id)
|
574
|
+
end
|
575
|
+
unless (mbox_id(LOST_FOUND_MBOX_NAME)) then
|
576
|
+
logger.warn('create lost+found mailbox.')
|
577
|
+
mbox_id = add_mbox(LOST_FOUND_MBOX_NAME)
|
578
|
+
yield(mbox_id)
|
579
|
+
end
|
580
|
+
|
581
|
+
logger.info('recovery phase 5: end.')
|
582
|
+
|
583
|
+
self
|
584
|
+
end
|
585
|
+
|
586
|
+
def recovery_phase6_msg_scan(mbox_db, logger: Logger.new(STDOUT))
|
587
|
+
logger.info('recovery phase 6: start.')
|
588
|
+
|
589
|
+
@kvs.each_key do |key|
|
590
|
+
if (msg_id = get_recover_entry(key, 'msg_id2mbox-') {|s| s.to_i }) then
|
591
|
+
is_modified = false
|
592
|
+
mbox_uid_map = msg_mbox_uid_mapping(msg_id)
|
593
|
+
mbox_uid_map.each_pair.to_a.each do |mbox_id, uid_set|
|
594
|
+
uid_set.to_a.each do |uid|
|
595
|
+
if (msg_id2 = mbox_db[mbox_id].msg_id(uid)) then
|
596
|
+
if (msg_id != msg_id2) then
|
597
|
+
logger.warn("lost+found message -> mailbox: #{msg_id} -> #{mbox_id},#{uid}")
|
598
|
+
is_modified = true
|
599
|
+
uid_set.delete(uid)
|
600
|
+
mbox_uid_map.delete(mbox_id) if uid_set.empty?
|
601
|
+
@lost_found_msg_set << msg_id
|
602
|
+
end
|
603
|
+
else
|
604
|
+
logger.warn("repair mailbox -> message: #{mbox_id},#{uid} -> #{msg_id}")
|
605
|
+
mbox_db[mbox_id].add_msg(uid, msg_id)
|
606
|
+
end
|
607
|
+
end
|
608
|
+
end
|
609
|
+
|
610
|
+
if (is_modified) then
|
611
|
+
logger.warn("save repaired message: #{msg_id}")
|
612
|
+
put_obj("msg_id2mbox-#{msg_id}", mbox_uid_map)
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
logger.info('recovery phase 6: end.')
|
618
|
+
|
619
|
+
self
|
620
|
+
end
|
621
|
+
|
622
|
+
def recovery_phase7_mbox_msg_scan(mbox_db, flag_name_list, logger: Logger.new(STDOUT))
|
623
|
+
logger.info('recovery phase 7: start.')
|
624
|
+
|
625
|
+
mbox_db.each_key do |mbox_id|
|
626
|
+
logger.info("scan mailbox: #{mbox_id}")
|
627
|
+
|
628
|
+
max_uid = 0
|
629
|
+
msg_num = 0
|
630
|
+
flag_num = Hash.new(0)
|
631
|
+
del_uid_list = []
|
632
|
+
|
633
|
+
mbox_db[mbox_id].each_msg_uid do |uid|
|
634
|
+
msg_id = mbox_db[mbox_id].msg_id(uid)
|
635
|
+
if ((mbox_uid_map = msg_mbox_uid_mapping(msg_id)) && (uid_set = mbox_uid_map[mbox_id]) && (uid_set.include? uid)) then
|
636
|
+
max_uid = uid if (max_uid < uid)
|
637
|
+
msg_num += 1
|
638
|
+
for name in flag_name_list
|
639
|
+
if (name == 'deleted') then
|
640
|
+
if (mbox_db[mbox_id].msg_flag_deleted(uid)) then
|
641
|
+
flag_num['deleted'] += 1
|
642
|
+
end
|
643
|
+
else
|
644
|
+
if (msg_flag(msg_id, name)) then
|
645
|
+
flag_num[name] += 1
|
646
|
+
end
|
647
|
+
end
|
648
|
+
end
|
649
|
+
else
|
650
|
+
del_uid_list << uid
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
for uid in del_uid_list
|
655
|
+
logger.warn("unlinked message uid: #{mbox_id},#{uid}")
|
656
|
+
mbox_db[mbox_id].set_msg_flag_deleted(uid, true)
|
657
|
+
mbox_db[mbox_id].expunge_msg(uid)
|
658
|
+
end
|
659
|
+
|
660
|
+
if (mbox_uid(mbox_id) <= max_uid) then
|
661
|
+
next_uid = max_uid + 1
|
662
|
+
logger.warn("repair mailbox uid: #{next_uid}")
|
663
|
+
put_num("mbox_id2uid-#{mbox_id}", next_uid)
|
664
|
+
end
|
665
|
+
|
666
|
+
if (mbox_msg_num(mbox_id) != msg_num) then
|
667
|
+
logger.warn("repair mailbox message number: #{msg_num}")
|
668
|
+
put_num("mbox_id2msgnum-#{mbox_id}", msg_num)
|
669
|
+
end
|
670
|
+
|
671
|
+
for name in flag_name_list
|
672
|
+
if (mbox_flag_num(mbox_id, name) != flag_num[name]) then
|
673
|
+
logger.warn("repair mailbox #{name} flag number: #{flag_num[name]}")
|
674
|
+
put_num("mbox_id2flagnum-#{mbox_id}-#{name}", flag_num[name])
|
675
|
+
end
|
676
|
+
end
|
677
|
+
end
|
678
|
+
|
679
|
+
logger.info('recovery phase 7: end.')
|
680
|
+
|
681
|
+
self
|
682
|
+
end
|
683
|
+
|
684
|
+
def recovery_phase8_lost_found(mbox_db, logger: Logger.new(STDOUT))
|
685
|
+
logger.info('recovery phase 8: start.')
|
686
|
+
|
687
|
+
mbox_id = mbox_id(LOST_FOUND_MBOX_NAME) or raise "not found a #{LOST_FOUND_MBOX_NAME} mailbox."
|
688
|
+
for msg_id in @lost_found_msg_set
|
689
|
+
logger.warn("repair lost+found message: #{msg_id}")
|
690
|
+
uid = add_msg_mbox_uid(msg_id, mbox_id)
|
691
|
+
mbox_db[mbox_id].add_msg(uid, msg_id)
|
692
|
+
end
|
693
|
+
|
694
|
+
logger.info('recovery phase 8: end.')
|
695
|
+
|
696
|
+
self
|
697
|
+
end
|
698
|
+
end
|
699
|
+
|
700
|
+
class Message < Core
|
701
|
+
def add_msg(msg_id, text)
|
702
|
+
put_str(msg_id.to_s, text)
|
703
|
+
self
|
704
|
+
end
|
705
|
+
|
706
|
+
def del_msg(msg_id)
|
707
|
+
@kvs.delete(msg_id.to_s) or raise "not found a message text for id: #{msg_id}"
|
708
|
+
self
|
709
|
+
end
|
710
|
+
|
711
|
+
def each_msg_id
|
712
|
+
return enum_for(:each_msg_id) unless block_given?
|
713
|
+
@kvs.each_key do |msg_id|
|
714
|
+
yield(msg_id.to_i)
|
715
|
+
end
|
716
|
+
self
|
717
|
+
end
|
718
|
+
|
719
|
+
def msg_text(msg_id)
|
720
|
+
get_str(msg_id.to_s)
|
721
|
+
end
|
722
|
+
|
723
|
+
def msg_exist?(msg_id)
|
724
|
+
@kvs.key? msg_id.to_s
|
725
|
+
end
|
726
|
+
end
|
727
|
+
|
728
|
+
class Mailbox < Core
|
729
|
+
def put_msg_id(uid, msg_id, deleted: false)
|
730
|
+
s = msg_id.to_s
|
731
|
+
s << ',deleted' if deleted
|
732
|
+
@kvs[uid.to_s] = s
|
733
|
+
self
|
734
|
+
end
|
735
|
+
private :put_msg_id
|
736
|
+
|
737
|
+
def add_msg(uid, msg_id)
|
738
|
+
put_msg_id(uid, msg_id)
|
739
|
+
self
|
740
|
+
end
|
741
|
+
|
742
|
+
def each_msg_uid
|
743
|
+
return enum_for(:each_msg_uid) unless block_given?
|
744
|
+
@kvs.each_key do |uid|
|
745
|
+
yield(uid.to_i)
|
746
|
+
end
|
747
|
+
self
|
748
|
+
end
|
749
|
+
|
750
|
+
def msg_exist?(uid)
|
751
|
+
@kvs.key? uid.to_s
|
752
|
+
end
|
753
|
+
|
754
|
+
def msg_id(uid)
|
755
|
+
if (s = @kvs[uid.to_s]) then
|
756
|
+
s.split(',', 2)[0].to_i
|
757
|
+
end
|
758
|
+
end
|
759
|
+
|
760
|
+
def msg_flag_deleted(uid)
|
761
|
+
if (s = @kvs[uid.to_s]) then
|
762
|
+
s.split(',', 2)[1] == 'deleted'
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
def set_msg_flag_deleted(uid, value)
|
767
|
+
msg_id = msg_id(uid) or raise "not found a message uid: #{uid}"
|
768
|
+
put_msg_id(uid, msg_id, deleted: value)
|
769
|
+
self
|
770
|
+
end
|
771
|
+
|
772
|
+
def expunge_msg(uid)
|
773
|
+
case (msg_flag_deleted(uid))
|
774
|
+
when true
|
775
|
+
# OK
|
776
|
+
when false
|
777
|
+
raise "not deleted flag at message uid: #{uid}"
|
778
|
+
when nil
|
779
|
+
raise "not found a message uid: #{uid}"
|
780
|
+
else
|
781
|
+
raise 'internal error.'
|
782
|
+
end
|
783
|
+
@kvs.delete(uid.to_s) or raise 'internal error.'
|
784
|
+
self
|
785
|
+
end
|
786
|
+
end
|
787
|
+
end
|
788
|
+
end
|
789
|
+
|
790
|
+
# Local Variables:
|
791
|
+
# mode: Ruby
|
792
|
+
# indent-tabs-mode: nil
|
793
|
+
# End:
|