rims 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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: