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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/ChangeLog +379 -0
  4. data/Gemfile +11 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +566 -0
  7. data/Rakefile +29 -0
  8. data/bin/rims +11 -0
  9. data/lib/rims.rb +45 -0
  10. data/lib/rims/auth.rb +133 -0
  11. data/lib/rims/cksum_kvs.rb +68 -0
  12. data/lib/rims/cmd.rb +809 -0
  13. data/lib/rims/daemon.rb +338 -0
  14. data/lib/rims/db.rb +793 -0
  15. data/lib/rims/error.rb +23 -0
  16. data/lib/rims/gdbm_kvs.rb +76 -0
  17. data/lib/rims/hash_kvs.rb +66 -0
  18. data/lib/rims/kvs.rb +101 -0
  19. data/lib/rims/lock.rb +151 -0
  20. data/lib/rims/mail_store.rb +663 -0
  21. data/lib/rims/passwd.rb +251 -0
  22. data/lib/rims/pool.rb +88 -0
  23. data/lib/rims/protocol.rb +71 -0
  24. data/lib/rims/protocol/decoder.rb +1469 -0
  25. data/lib/rims/protocol/parser.rb +1114 -0
  26. data/lib/rims/rfc822.rb +456 -0
  27. data/lib/rims/server.rb +567 -0
  28. data/lib/rims/test.rb +391 -0
  29. data/lib/rims/version.rb +11 -0
  30. data/load_test/Rakefile +93 -0
  31. data/rims.gemspec +38 -0
  32. data/test/test_auth.rb +174 -0
  33. data/test/test_cksum_kvs.rb +121 -0
  34. data/test/test_config.rb +533 -0
  35. data/test/test_daemon_status_file.rb +169 -0
  36. data/test/test_daemon_waitpid.rb +72 -0
  37. data/test/test_db.rb +602 -0
  38. data/test/test_db_recovery.rb +732 -0
  39. data/test/test_error.rb +97 -0
  40. data/test/test_gdbm_kvs.rb +32 -0
  41. data/test/test_hash_kvs.rb +116 -0
  42. data/test/test_lock.rb +161 -0
  43. data/test/test_mail_store.rb +750 -0
  44. data/test/test_passwd.rb +203 -0
  45. data/test/test_protocol.rb +91 -0
  46. data/test/test_protocol_auth.rb +121 -0
  47. data/test/test_protocol_decoder.rb +6490 -0
  48. data/test/test_protocol_fetch.rb +994 -0
  49. data/test/test_protocol_request.rb +332 -0
  50. data/test/test_protocol_search.rb +974 -0
  51. data/test/test_rfc822.rb +696 -0
  52. metadata +174 -0
@@ -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:
@@ -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: