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,23 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module RIMS
4
+ class Error < StandardError
5
+ def self.suppress_2nd_error_at_resource_closing(logger: nil)
6
+ if ($!) then
7
+ begin
8
+ yield
9
+ rescue # not mask the first error
10
+ logger.error($!) if logger
11
+ nil
12
+ end
13
+ else
14
+ yield
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ # Local Variables:
21
+ # mode: Ruby
22
+ # indent-tabs-mode: nil
23
+ # End:
@@ -0,0 +1,76 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'gdbm'
4
+
5
+ module RIMS
6
+ class GDBM_KeyValueStore < KeyValueStore
7
+ def initialize(gdbm, path)
8
+ @db = gdbm
9
+ @path = path
10
+ end
11
+
12
+ class << self
13
+ def exist?(path)
14
+ gdbm_path = path + '.gdbm'
15
+ File.exist? gdbm_path
16
+ end
17
+
18
+ def open(path, *optional)
19
+ gdbm_path = path + '.gdbm'
20
+ new(GDBM.new(gdbm_path, *optional), gdbm_path)
21
+ end
22
+
23
+ def open_with_conf(name, config)
24
+ open(name)
25
+ end
26
+ end
27
+
28
+ def [](key)
29
+ @db[key]
30
+ end
31
+
32
+ def []=(key, value)
33
+ @db[key] = value
34
+ end
35
+
36
+ def delete(key)
37
+ @db.delete(key)
38
+ end
39
+
40
+ def key?(key)
41
+ @db.key? key
42
+ end
43
+
44
+ def each_key
45
+ return enum_for(:each_key) unless block_given?
46
+ @db.each_key do |key|
47
+ yield(key)
48
+ end
49
+ self
50
+ end
51
+
52
+ def sync
53
+ @db.sync
54
+ self
55
+ end
56
+
57
+ def close
58
+ @db.close
59
+ self
60
+ end
61
+
62
+ def destroy
63
+ unless (@db.closed?) then
64
+ raise "failed to destroy gdbm that isn't closed: #{@path}"
65
+ end
66
+ File.delete(@path)
67
+ nil
68
+ end
69
+ end
70
+ KeyValueStore::FactoryBuilder.add_plug_in('gdbm', GDBM_KeyValueStore)
71
+ end
72
+
73
+ # Local Variables:
74
+ # mode: Ruby
75
+ # indent-tabs-mode: nil
76
+ # End:
@@ -0,0 +1,66 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module RIMS
4
+ class Hash_KeyValueStore < KeyValueStore
5
+ def initialize(hash)
6
+ @db = hash
7
+ end
8
+
9
+ def [](key)
10
+ unless (key.is_a? String) then
11
+ raise "not a string key: #{key}"
12
+ end
13
+ @db[key.b]
14
+ end
15
+
16
+ def []=(key, value)
17
+ unless (key.is_a? String) then
18
+ raise "not a string key: #{key}"
19
+ end
20
+ unless (value.is_a? String) then
21
+ raise "not a string value: #{value}"
22
+ end
23
+ @db[key.b] = value.b
24
+ end
25
+
26
+ def delete(key)
27
+ unless (key.is_a? String) then
28
+ raise "not a string key: #{key}"
29
+ end
30
+ @db.delete(key.b)
31
+ end
32
+
33
+ def key?(key)
34
+ unless (key.is_a? String) then
35
+ raise "not a string key: #{key}"
36
+ end
37
+ @db.key? key.b
38
+ end
39
+
40
+ def each_key
41
+ return enum_for(:each_key) unless block_given?
42
+ @db.each_key do |key|
43
+ yield(key)
44
+ end
45
+ self
46
+ end
47
+
48
+ def sync
49
+ self
50
+ end
51
+
52
+ def close
53
+ @db = nil
54
+ self
55
+ end
56
+
57
+ def destroy
58
+ self
59
+ end
60
+ end
61
+ end
62
+
63
+ # Local Variables:
64
+ # mode: Ruby
65
+ # indent-tabs-mode: nil
66
+ # End:
@@ -0,0 +1,101 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module RIMS
4
+ class KeyValueStore
5
+ def [](key)
6
+ raise NotImplementedError, 'abstract'
7
+ end
8
+
9
+ def []=(key, value)
10
+ raise NotImplementedError, 'abstract'
11
+ end
12
+
13
+ def delete(key)
14
+ raise NotImplementedError, 'abstract'
15
+ end
16
+
17
+ def key?(key)
18
+ raise NotImplementedError, 'abstract'
19
+ end
20
+
21
+ def each_key
22
+ raise NotImplementedError, 'abstract'
23
+ end
24
+
25
+ def each_value
26
+ return enum_for(:each_value) unless block_given?
27
+ each_key do |key|
28
+ yield(self[key])
29
+ end
30
+ end
31
+
32
+ def each_pair
33
+ return enum_for(:each_pair) unless block_given?
34
+ each_key do |key|
35
+ yield(key, self[key])
36
+ end
37
+ end
38
+
39
+ def sync
40
+ raise NotImplementedError, 'abstract'
41
+ end
42
+
43
+ def close
44
+ raise NotImplementedError, 'abstract'
45
+ end
46
+
47
+ def destroy
48
+ raise NotImplementedError, 'abstract'
49
+ end
50
+
51
+ def self.exist?(path)
52
+ raise NotImplementedError, 'not implemented.'
53
+ end
54
+
55
+ def self.open_with_conf(config)
56
+ raise NotImplementedError, 'not implemented.'
57
+ end
58
+
59
+ class FactoryBuilder
60
+ PLUG_IN = {} # :nodoc:
61
+
62
+ class << self
63
+ def add_plug_in(name, klass)
64
+ PLUG_IN[name] = klass
65
+ self
66
+ end
67
+
68
+ def get_plug_in(name)
69
+ PLUG_IN[name] or raise KeyError, "not found a key-value store plug-in: #{name}"
70
+ end
71
+ end
72
+
73
+ def initialize
74
+ @open = nil
75
+ @factory = proc{|name|
76
+ @open.call(name)
77
+ }
78
+ end
79
+
80
+ attr_reader :factory
81
+
82
+ def open(&block) # :yields: name
83
+ @open = block
84
+ self
85
+ end
86
+
87
+ def use(middleware, *args, &block)
88
+ prev_factory = @factory
89
+ @factory = proc{|name|
90
+ middleware.new(prev_factory.call(name), *args, &block)
91
+ }
92
+ self
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ # Local Variables:
99
+ # mode: Ruby
100
+ # indent-tabs-mode: nil
101
+ # End:
@@ -0,0 +1,151 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'logger'
4
+
5
+ module RIMS
6
+ class LockError < Error
7
+ end
8
+
9
+ class IllegalLockError < LockError
10
+ end
11
+
12
+ class ReadLockError < LockError
13
+ end
14
+
15
+ class ReadLockTimeoutError < ReadLockError
16
+ end
17
+
18
+ class WriteLockError < LockError
19
+ end
20
+
21
+ class WriteLockTimeoutError < LockError
22
+ end
23
+
24
+ class ReadWriteLock
25
+ DEFAULT_TIMEOUT_SECONDS = 300
26
+
27
+ def initialize
28
+ @lock = Thread::Mutex.new
29
+ @read_cond = Thread::ConditionVariable.new
30
+ @write_cond = Thread::ConditionVariable.new
31
+ @count_of_working_readers = 0
32
+ @count_of_standby_writers = 0
33
+ @prefer_to_writer = true
34
+ @writing = false
35
+ end
36
+
37
+ def read_lock(timeout_seconds=DEFAULT_TIMEOUT_SECONDS)
38
+ time_limit = Time.now + timeout_seconds
39
+ @lock.synchronize{
40
+ while (@writing || (@prefer_to_writer && @count_of_standby_writers > 0))
41
+ if (timeout_seconds > 0) then
42
+ @read_cond.wait(@lock, timeout_seconds)
43
+ else
44
+ raise ReadLockTimeoutError, 'read-lock wait timeout'
45
+ end
46
+ timeout_seconds = time_limit - Time.now
47
+ end
48
+ @count_of_working_readers += 1
49
+ }
50
+ nil
51
+ end
52
+
53
+ def read_unlock
54
+ @lock.synchronize{
55
+ @count_of_working_readers -= 1
56
+ @count_of_working_readers >= 0 or raise IllegalLockError, 'illegal read lock pattern: lock/unlock/unlock'
57
+ @prefer_to_writer = true
58
+ if (@count_of_standby_writers > 0) then
59
+ @write_cond.signal
60
+ end
61
+ }
62
+ nil
63
+ end
64
+
65
+ def read_synchronize(timeout_seconds=DEFAULT_TIMEOUT_SECONDS)
66
+ read_lock(timeout_seconds)
67
+ begin
68
+ yield
69
+ ensure
70
+ read_unlock
71
+ end
72
+ end
73
+
74
+ def write_lock(timeout_seconds=DEFAULT_TIMEOUT_SECONDS)
75
+ time_limit = Time.now + timeout_seconds
76
+ @lock.synchronize{
77
+ @count_of_standby_writers += 1
78
+ begin
79
+ while (@writing || @count_of_working_readers > 0)
80
+ if (timeout_seconds > 0) then
81
+ @write_cond.wait(@lock, timeout_seconds)
82
+ else
83
+ raise WriteLockTimeoutError, 'write-lock wait timeout'
84
+ end
85
+ timeout_seconds = time_limit - Time.now
86
+ end
87
+ @writing = true
88
+ ensure
89
+ @count_of_standby_writers -= 1
90
+ end
91
+ }
92
+ nil
93
+ end
94
+
95
+ def write_unlock
96
+ @lock.synchronize{
97
+ @writing or raise IllegalLockError, 'illegal write lock pattern: lock/unlock/unlock'
98
+ @writing = false
99
+ @prefer_to_writer = false
100
+ @read_cond.broadcast
101
+ if (@count_of_standby_writers > 0) then
102
+ @write_cond.signal
103
+ end
104
+ }
105
+ nil
106
+ end
107
+
108
+ def write_synchronize(timeout_seconds=DEFAULT_TIMEOUT_SECONDS)
109
+ write_lock(timeout_seconds)
110
+ begin
111
+ yield
112
+ ensure
113
+ write_unlock
114
+ end
115
+ end
116
+
117
+ # compatible for Thread::Mutex
118
+ alias synchronize write_synchronize
119
+
120
+ def self.write_lock_timeout_detach(first_timeout_seconds, detached_timeout_seconds, logger: Logger.new(STDOUT)) # yields: timeout_seconds
121
+ begin
122
+ logger.debug('ready to detach write-lock timeout.')
123
+ yield(first_timeout_seconds)
124
+ logger.debug('not detached write-lock timeout.')
125
+ nil
126
+ rescue WriteLockTimeoutError
127
+ logger.warn($!)
128
+ Thread.new{
129
+ begin
130
+ logger.warn('detached write-lock timeout.')
131
+ yield(detached_timeout_seconds)
132
+ logger.info('detached write-lock timeout thread is completed.')
133
+ nil
134
+ rescue WriteLockTimeoutError
135
+ logger.warn($!)
136
+ retry
137
+ rescue
138
+ logger.error('unexpected error at a detached thread and give up to retry write-lock timeout error.')
139
+ logger.error($!)
140
+ $!
141
+ end
142
+ }
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ # Local Variables:
149
+ # mode: Ruby
150
+ # indent-tabs-mode: nil
151
+ # End:
@@ -0,0 +1,663 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'forwardable'
4
+ require 'logger'
5
+ require 'set'
6
+ require 'thread'
7
+
8
+ module RIMS
9
+ class MailStore
10
+ MSG_FLAG_NAMES = %w[ answered flagged deleted seen draft recent ].each{|n| n.freeze }.freeze
11
+
12
+ def initialize(meta_db, msg_db, &mbox_db_factory) # :yields: mbox_id
13
+ @meta_db = meta_db
14
+ @msg_db = msg_db
15
+ @mbox_db_factory = mbox_db_factory
16
+
17
+ @mbox_db = {}
18
+ @meta_db.each_mbox_id do |mbox_id|
19
+ @mbox_db[mbox_id] = nil
20
+ end
21
+
22
+ if (@meta_db.dirty?) then
23
+ @abort_transaction = true
24
+ else
25
+ @abort_transaction = false
26
+ @meta_db.dirty = true
27
+ end
28
+
29
+ @server_response_queue_pool = RIMS::ObjectPool.new{|object_pool, mbox_id, object_lock|
30
+ MailboxServerResponseQueueBundleHolder.new(object_pool, mbox_id, object_lock)
31
+ }
32
+ end
33
+
34
+ def get_mbox_db(mbox_id)
35
+ if (@mbox_db.key? mbox_id) then
36
+ @mbox_db[mbox_id] ||= @mbox_db_factory.call(mbox_id)
37
+ end
38
+ end
39
+ private :get_mbox_db
40
+
41
+ def abort_transaction?
42
+ @abort_transaction
43
+ end
44
+
45
+ def transaction
46
+ if (@abort_transaction) then
47
+ raise 'abort transaction.'
48
+ end
49
+
50
+ transaction_completed = false
51
+ begin
52
+ return_value = yield
53
+ transaction_completed = true
54
+ ensure
55
+ @abort_transaction = true unless transaction_completed
56
+ end
57
+
58
+ return_value
59
+ end
60
+
61
+ def recovery_data(logger: Logger.new(STDOUT))
62
+ begin
63
+ logger.info('test read all: meta DB')
64
+ @meta_db.test_read_all do |error|
65
+ logger.error("read fail: #{error}")
66
+ end
67
+ logger.info('test read all: msg DB')
68
+ @msg_db.test_read_all do |error|
69
+ logger.error("read fail: #{error}")
70
+ end
71
+ @mbox_db.each_key do |mbox_id|
72
+ logger.info("test_read_all: mailbox DB #{mbox_id}")
73
+ get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
74
+ @mbox_db[mbox_id].test_read_all do |error|
75
+ logger.error("read fail: #{error}")
76
+ end
77
+ end
78
+
79
+ @meta_db.recovery_start
80
+ @meta_db.recovery_phase1_msg_scan(@msg_db, logger: logger)
81
+ @meta_db.recovery_phase2_msg_scan(@msg_db, logger: logger)
82
+ @meta_db.recovery_phase3_mbox_scan(logger: logger)
83
+ @meta_db.recovery_phase4_mbox_scan(logger: logger)
84
+ @meta_db.recovery_phase5_mbox_repair(logger: logger) {|mbox_id|
85
+ if (@mbox_db.key? mbox_id) then
86
+ raise "not a lost mailbox: #{mbox_id}"
87
+ else
88
+ @mbox_db[mbox_id] = nil
89
+ get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
90
+ end
91
+ }
92
+ @meta_db.recovery_phase6_msg_scan(@mbox_db, logger: logger)
93
+ @meta_db.recovery_phase7_mbox_msg_scan(@mbox_db, MSG_FLAG_NAMES, logger: logger)
94
+ @meta_db.recovery_phase8_lost_found(@mbox_db, logger: logger)
95
+ @meta_db.recovery_end
96
+ ensure
97
+ @abort_transaction = ! $!.nil?
98
+ end
99
+
100
+ self
101
+ end
102
+
103
+ def close
104
+ @mbox_db.each_value do |db|
105
+ db.close if db
106
+ end
107
+ @msg_db.close
108
+ @meta_db.dirty = false unless @abort_transaction
109
+ @meta_db.close
110
+ self
111
+ end
112
+
113
+ def sync
114
+ transaction{
115
+ @msg_db.sync
116
+ @mbox_db.each_value do |db|
117
+ db.sync if db
118
+ end
119
+ @meta_db.sync
120
+ self
121
+ }
122
+ end
123
+
124
+ def cnum
125
+ @meta_db.cnum
126
+ end
127
+
128
+ def uid(mbox_id)
129
+ @meta_db.mbox_uid(mbox_id)
130
+ end
131
+
132
+ def uidvalidity
133
+ @meta_db.uidvalidity
134
+ end
135
+
136
+ def add_mbox(name)
137
+ transaction{
138
+ name = 'INBOX' if (name =~ /\AINBOX\z/i)
139
+ name = name.b
140
+
141
+ mbox_id = @meta_db.add_mbox(name)
142
+ @mbox_db[mbox_id] = nil
143
+
144
+ @meta_db.cnum_succ!
145
+
146
+ mbox_id
147
+ }
148
+ end
149
+
150
+ def del_mbox(mbox_id)
151
+ transaction{
152
+ mbox_name = @meta_db.mbox_name(mbox_id) or raise "not found a mailbox: #{mbox_id}."
153
+
154
+ get_mbox_db(mbox_id)
155
+ mbox_db = @mbox_db.delete(mbox_id)
156
+ mbox_db.each_msg_uid do |uid|
157
+ msg_id = mbox_db.msg_id(uid)
158
+ del_msg(msg_id, mbox_id, uid)
159
+ end
160
+ mbox_db.close
161
+ mbox_db.destroy
162
+
163
+ for name in MSG_FLAG_NAMES
164
+ @meta_db.clear_mbox_flag_num(mbox_id, name)
165
+ end
166
+ @meta_db.del_mbox(mbox_id) or raise 'internal error.'
167
+
168
+ @meta_db.cnum_succ!
169
+
170
+ mbox_name
171
+ }
172
+ end
173
+
174
+ def rename_mbox(mbox_id, new_name)
175
+ transaction{
176
+ old_name = @meta_db.mbox_name(mbox_id) or raise "not found a mailbox: #{mbox_id}."
177
+ old_name = old_name.dup.force_encoding('utf-8')
178
+
179
+ new_name = 'INBOX' if (new_name =~ /\AINBOX\z/i)
180
+ @meta_db.rename_mbox(mbox_id, new_name.b)
181
+
182
+ @meta_db.cnum_succ!
183
+
184
+ old_name
185
+ }
186
+ end
187
+
188
+ def mbox_name(mbox_id)
189
+ if (name = @meta_db.mbox_name(mbox_id)) then
190
+ name.dup.force_encoding('utf-8')
191
+ end
192
+ end
193
+
194
+ def mbox_id(mbox_name)
195
+ mbox_name = 'INBOX' if (mbox_name =~ /\AINBOX\z/i)
196
+ @meta_db.mbox_id(mbox_name.b)
197
+ end
198
+
199
+ def each_mbox_id
200
+ return enum_for(:each_mbox_id) unless block_given?
201
+ @meta_db.each_mbox_id do |mbox_id|
202
+ yield(mbox_id)
203
+ end
204
+ self
205
+ end
206
+
207
+ def mbox_msg_num(mbox_id)
208
+ @meta_db.mbox_msg_num(mbox_id)
209
+ end
210
+
211
+ def mbox_flag_num(mbox_id, flag_name)
212
+ if (MSG_FLAG_NAMES.include? flag_name) then
213
+ @meta_db.mbox_flag_num(mbox_id, flag_name)
214
+ else
215
+ raise "unknown flag name: #{name}"
216
+ end
217
+ end
218
+
219
+ def add_msg(mbox_id, msg_text, msg_date=Time.now)
220
+ transaction{
221
+ mbox_db = get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
222
+
223
+ msg_id = @meta_db.msg_id_succ!
224
+ @msg_db.add_msg(msg_id, msg_text)
225
+ @meta_db.set_msg_date(msg_id, msg_date)
226
+ @meta_db.set_msg_flag(msg_id, 'recent', true)
227
+
228
+ uid = @meta_db.add_msg_mbox_uid(msg_id, mbox_id)
229
+ mbox_db.add_msg(uid, msg_id)
230
+
231
+ @meta_db.cnum_succ!
232
+
233
+ uid
234
+ }
235
+ end
236
+
237
+ def del_msg(msg_id, mbox_id, uid)
238
+ mbox_uid_map = @meta_db.del_msg_mbox_uid(msg_id, mbox_id, uid)
239
+ if (mbox_uid_map.empty?) then
240
+ @meta_db.clear_msg_date(msg_id)
241
+ @meta_db.clear_msg_flag(msg_id)
242
+ @meta_db.clear_msg_mbox_uid_mapping(msg_id)
243
+ @msg_db.del_msg(msg_id)
244
+ end
245
+ @meta_db.mbox_flag_num_decrement(mbox_id, 'deleted')
246
+ nil
247
+ end
248
+ private :del_msg
249
+
250
+ def copy_msg(src_uid, src_mbox_id, dst_mbox_id)
251
+ transaction{
252
+ src_mbox_db = get_mbox_db(src_mbox_id) or raise "not found a source mailbox: #{src_mbox_id}"
253
+ dst_mbox_db = get_mbox_db(dst_mbox_id) or raise "not found a destination mailbox: #{dst_mbox_id}"
254
+
255
+ msg_id = src_mbox_db.msg_id(src_uid) or raise "not found a message: #{src_mbox_id},#{src_uid}"
256
+ dst_uid = @meta_db.add_msg_mbox_uid(msg_id, dst_mbox_id)
257
+ dst_mbox_db.add_msg(dst_uid, msg_id)
258
+
259
+ @meta_db.cnum_succ!
260
+
261
+ dst_uid
262
+ }
263
+ end
264
+
265
+ def msg_exist?(mbox_id, uid)
266
+ mbox_db = get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
267
+ mbox_db.msg_exist? uid
268
+ end
269
+
270
+ def msg_text(mbox_id, uid)
271
+ mbox_db = get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
272
+ msg_id = mbox_db.msg_id(uid) or raise "not found a message: #{mbox_id},#{uid}"
273
+ @msg_db.msg_text(msg_id)
274
+ end
275
+
276
+ def msg_date(mbox_id, uid)
277
+ mbox_db = get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
278
+ msg_id = mbox_db.msg_id(uid) or raise "not found a message: #{mbox_id},#{uid}"
279
+ @meta_db.msg_date(msg_id)
280
+ end
281
+
282
+ def msg_flag(mbox_id, uid, flag_name)
283
+ mbox_db = get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
284
+
285
+ if ((MSG_FLAG_NAMES - %w[ deleted ]).include? flag_name) then
286
+ msg_id = mbox_db.msg_id(uid) or raise "not found a message: #{mbox_id},#{uid}"
287
+ @meta_db.msg_flag(msg_id, flag_name)
288
+ elsif (flag_name == 'deleted') then
289
+ mbox_db.msg_flag_deleted(uid)
290
+ else
291
+ raise "unknown flag name: #{flag_name}"
292
+ end
293
+ end
294
+
295
+ def set_msg_flag(mbox_id, uid, flag_name, flag_value)
296
+ transaction{
297
+ mbox_db = get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
298
+
299
+ if ((MSG_FLAG_NAMES - %w[ deleted ]).include? flag_name) then
300
+ msg_id = mbox_db.msg_id(uid) or raise "not found a message: #{mbox_id},#{uid}"
301
+ @meta_db.set_msg_flag(msg_id, flag_name, flag_value)
302
+ elsif (flag_name == 'deleted') then
303
+ prev_deleted = mbox_db.msg_flag_deleted(uid)
304
+ mbox_db.set_msg_flag_deleted(uid, flag_value)
305
+ if (! prev_deleted && flag_value) then
306
+ @meta_db.mbox_flag_num_increment(mbox_id, 'deleted')
307
+ elsif (prev_deleted && ! flag_value) then
308
+ @meta_db.mbox_flag_num_decrement(mbox_id, 'deleted')
309
+ end
310
+ else
311
+ raise "unknown flag name: #{flag_name}"
312
+ end
313
+
314
+ @meta_db.cnum_succ!
315
+
316
+ self
317
+ }
318
+ end
319
+
320
+ def each_msg_uid(mbox_id)
321
+ mbox_db = get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
322
+ return enum_for(:each_msg_uid, mbox_id) unless block_given?
323
+ mbox_db.each_msg_uid do |uid|
324
+ yield(uid)
325
+ end
326
+ self
327
+ end
328
+
329
+ def expunge_mbox(mbox_id)
330
+ transaction{
331
+ mbox_db = get_mbox_db(mbox_id) or raise "not found a mailbox: #{mbox_id}."
332
+
333
+ uid_list = mbox_db.each_msg_uid.find_all{|uid| mbox_db.msg_flag_deleted(uid) }
334
+ msg_id_list = uid_list.map{|uid| mbox_db.msg_id(uid) }
335
+
336
+ uid_list.zip(msg_id_list) do |uid, msg_id|
337
+ mbox_db.expunge_msg(uid)
338
+ del_msg(msg_id, mbox_id, uid)
339
+ yield(uid) if block_given?
340
+ end
341
+
342
+ @meta_db.cnum_succ!
343
+
344
+ self
345
+ }
346
+ end
347
+
348
+ def select_mbox(mbox_id)
349
+ @meta_db.mbox_name(mbox_id) or raise "not found a mailbox: #{mbox_id}."
350
+ MailFolder.new(mbox_id, self).attach_server_response_queue(@server_response_queue_pool)
351
+ end
352
+
353
+ def examine_mbox(mbox_id)
354
+ @meta_db.mbox_name(mbox_id) or raise "not found a mailbox: #{mbox_id}."
355
+ MailFolder.new(mbox_id, self, read_only: true).attach_server_response_queue(@server_response_queue_pool)
356
+ end
357
+
358
+ def self.build_pool(kvs_meta_open, kvs_text_open)
359
+ RIMS::ObjectPool.new{|object_pool, unique_user_id, object_lock|
360
+ RIMS::MailStoreHolder.build(object_pool, unique_user_id, object_lock, kvs_meta_open, kvs_text_open)
361
+ }
362
+ end
363
+ end
364
+
365
+ class MailFolder
366
+ MessageStruct = Struct.new(:uid, :num)
367
+
368
+ def initialize(mbox_id, mail_store, read_only: false)
369
+ @mbox_id = mbox_id
370
+ @mail_store = mail_store
371
+ @read_only = read_only
372
+ @mail_folder_key = object_id
373
+
374
+ # late loding
375
+ @cnum = nil
376
+ @msg_list = nil
377
+ @uid_map = nil
378
+ end
379
+
380
+ def attach_server_response_queue(server_response_queue_pool)
381
+ @server_response_queue_bundle = server_response_queue_pool.get(@mbox_id)
382
+ @server_response_queue = @server_response_queue_bundle.attach_queue(@mail_folder_key)
383
+ self
384
+ end
385
+
386
+ def server_response_multicast_push(server_response_message)
387
+ @server_response_queue_bundle.multicast_push(server_response_message, @mail_folder_key)
388
+ self
389
+ end
390
+
391
+ def server_response?
392
+ ! @server_response_queue.empty?
393
+ end
394
+
395
+ def server_response_fetch
396
+ while (server_response?)
397
+ server_response_message = @server_response_queue.pop(true)
398
+ yield(server_response_message)
399
+ end
400
+ self
401
+ end
402
+
403
+ def server_response_idle_wait
404
+ catch(:server_response_idle_wait_interrupt) {
405
+ while (server_response_message = @server_response_queue.pop(false))
406
+ server_response_list = [ server_response_message ]
407
+ server_response_fetch{|next_response_message|
408
+ if (next_response_message) then
409
+ server_response_list.push(next_response_message)
410
+ else
411
+ yield(server_response_list)
412
+ throw(:server_response_idle_wait_interrupt)
413
+ end
414
+ }
415
+ yield(server_response_list)
416
+ end
417
+ }
418
+ self
419
+ end
420
+
421
+ def server_response_idle_interrupt
422
+ @server_response_queue.push(nil)
423
+ end
424
+
425
+ def reload
426
+ @cnum = @mail_store.cnum
427
+
428
+ msg_id_list = @mail_store.each_msg_uid(@mbox_id).to_a
429
+ msg_id_list.sort!
430
+
431
+ @msg_list = Array.new(msg_id_list.length)
432
+ @uid_map = {}
433
+
434
+ msg_id_list.each_with_index do |id, i|
435
+ num = i.succ
436
+ msg = MessageStruct.new(id, num)
437
+ @msg_list[i] = msg
438
+ @uid_map[id] = msg
439
+ end
440
+
441
+ self
442
+ end
443
+
444
+ def updated?
445
+ @mail_store.cnum != @cnum
446
+ end
447
+
448
+ attr_reader :mbox_id
449
+
450
+ def [](msg_idx)
451
+ @msg_list[msg_idx]
452
+ end
453
+
454
+ def each_msg
455
+ return enum_for(:each_msg) unless block_given?
456
+ for msg in @msg_list
457
+ yield(msg)
458
+ end
459
+ self
460
+ end
461
+
462
+ def msg_find_all(msg_set, uid: false)
463
+ if (msg_set.size < @msg_list.length) then
464
+ if (uid) then
465
+ msg_set.inject([]) {|msg_list, id|
466
+ if (msg = @uid_map[id]) then
467
+ msg_list << msg
468
+ end
469
+ msg_list
470
+ }
471
+ else
472
+ msg_set.inject([]) {|msg_list, num|
473
+ if (1 <= num && num <= @msg_list.length) then
474
+ msg_list << @msg_list[num - 1]
475
+ end
476
+ msg_list
477
+ }
478
+ end
479
+ else
480
+ if (uid) then
481
+ @msg_list.find_all{|msg|
482
+ msg_set.include? msg.uid
483
+ }
484
+ else
485
+ @msg_list.find_all{|msg|
486
+ msg_set.include? msg.num
487
+ }
488
+ end
489
+ end
490
+ end
491
+
492
+ attr_reader :read_only
493
+ alias read_only? read_only
494
+
495
+ def expunge_mbox
496
+ if (@mail_store.mbox_flag_num(@mbox_id, 'deleted') > 0) then
497
+ if (block_given?) then
498
+ uid2num = {}
499
+ for msg in @msg_list
500
+ uid2num[msg.uid] = msg.num
501
+ end
502
+
503
+ msg_num_list = []
504
+ @mail_store.expunge_mbox(@mbox_id) do |uid|
505
+ num = uid2num[uid] or raise "internal error: not found a message: #{@mbox_id},#{uid}"
506
+ msg_num_list << num
507
+ end
508
+
509
+ # to prevent to decrement message sequence numbers that
510
+ # appear in a set of successive expunge responses, expunge
511
+ # command should early return an expunge response of larger
512
+ # message sequence number.
513
+ msg_num_list.sort!
514
+ msg_num_list.reverse_each do |num|
515
+ yield(num)
516
+ end
517
+ else
518
+ @mail_store.expunge_mbox(@mbox_id)
519
+ end
520
+ end
521
+
522
+ self
523
+ end
524
+
525
+ def close(&block)
526
+ unless (@read_only) then
527
+ expunge_mbox(&block)
528
+ @mail_store.each_msg_uid(@mbox_id) do |msg_id|
529
+ if (@mail_store.msg_flag(@mbox_id, msg_id, 'recent')) then
530
+ @mail_store.set_msg_flag(@mbox_id, msg_id, 'recent', false)
531
+ end
532
+ end
533
+ end
534
+ @mail_store = nil
535
+
536
+ @server_response_queue_bundle.detach_queue(@mail_folder_key)
537
+ @server_response_queue_bundle.return_pool
538
+ @server_response_queue_bundle = nil
539
+
540
+ self
541
+ end
542
+
543
+ def parse_msg_set(msg_set_desc, uid: false)
544
+ if (@msg_list.empty?) then
545
+ last_number = 0
546
+ else
547
+ if (uid) then
548
+ last_number = @msg_list[-1].uid
549
+ else
550
+ last_number = @msg_list[-1].num
551
+ end
552
+ end
553
+
554
+ self.class.parse_msg_set(msg_set_desc, last_number)
555
+ end
556
+
557
+ def self.parse_msg_seq(msg_seq_desc, last_number)
558
+ case (msg_seq_desc)
559
+ when /\A(\d+|\*)\z/
560
+ msg_seq_pair = [ $&, $& ]
561
+ when /\A(\d+|\*):(\d+|\*)\z/
562
+ msg_seq_pair = [ $1, $2 ]
563
+ else
564
+ raise MessageSetSyntaxError, "invalid message sequence format: #{msg_seq_desc}"
565
+ end
566
+
567
+ msg_seq_pair.map!{|num|
568
+ case (num)
569
+ when '*'
570
+ last_number
571
+ else
572
+ n = num.to_i
573
+ if (n < 1) then
574
+ raise MessageSetSyntaxError, "out of range of message sequence number: #{msg_seq_desc}"
575
+ end
576
+ n
577
+ end
578
+ }
579
+
580
+ Range.new(msg_seq_pair[0], msg_seq_pair[1])
581
+ end
582
+
583
+ def self.parse_msg_set(msg_set_desc, last_number)
584
+ msg_set = [].to_set
585
+ msg_set_desc.split(/,/).each do |msg_seq_desc|
586
+ msg_range = parse_msg_seq(msg_seq_desc, last_number)
587
+ msg_range.step do |n|
588
+ msg_set << n
589
+ end
590
+ end
591
+
592
+ msg_set
593
+ end
594
+ end
595
+
596
+ class MailStoreHolder < ObjectPool::ObjectHolder
597
+ extend Forwardable
598
+
599
+ def self.build(object_pool, unique_user_id, object_lock, kvs_meta_open, kvs_text_open)
600
+ kvs_build = proc{|kvs_open, db_name|
601
+ kvs_open.call(MAILBOX_DATA_STRUCTURE_VERSION, unique_user_id, db_name)
602
+ }
603
+
604
+ mail_store = MailStore.new(DB::Meta.new(kvs_build.call(kvs_meta_open, 'meta')),
605
+ DB::Message.new(kvs_build.call(kvs_text_open, 'message'))) {|mbox_id|
606
+ DB::Mailbox.new(kvs_build.call(kvs_meta_open, "mailbox_#{mbox_id}"))
607
+ }
608
+ mail_store.add_mbox('INBOX') unless mail_store.mbox_id('INBOX')
609
+
610
+ new(object_pool, unique_user_id, object_lock, mail_store)
611
+ end
612
+
613
+ def initialize(object_pool, unique_user_id, object_lock, mail_store)
614
+ super(object_pool, unique_user_id)
615
+ @object_lock = object_lock
616
+ @mail_store = mail_store
617
+ end
618
+
619
+ alias unique_user_id object_key
620
+ attr_reader :mail_store
621
+
622
+ def_delegator :@object_lock, :read_synchronize
623
+ def_delegator :@object_lock, :write_synchronize
624
+
625
+ def object_destroy
626
+ @mail_store.close
627
+ end
628
+ end
629
+
630
+ class MailboxServerResponseQueueBundleHolder < ObjectPool::ObjectHolder
631
+ def initialize(object_pool, mbox_id, object_lock)
632
+ super(object_pool, mbox_id)
633
+ @object_lock = object_lock
634
+ @queue_map = Hash.new{|h, k| h[k] = Thread::Queue.new }
635
+ end
636
+
637
+ alias mbox_id object_id
638
+
639
+ def attach_queue(mail_folder_key)
640
+ @object_lock.write_synchronize{ @queue_map[mail_folder_key] }
641
+ end
642
+
643
+ def detach_queue(mail_folder_key)
644
+ @object_lock.write_synchronize{ @queue_map.delete(mail_folder_key) } or raise "not found a queue at mail folder key: #{mail_folder_key}"
645
+ self
646
+ end
647
+
648
+ def multicast_push(server_response_message, this_mail_folder_key)
649
+ @object_lock.read_synchronize{
650
+ for mail_folder_key, queue in @queue_map
651
+ next if (mail_folder_key == this_mail_folder_key)
652
+ queue.push(server_response_message)
653
+ end
654
+ }
655
+ self
656
+ end
657
+ end
658
+ end
659
+
660
+ # Local Variables:
661
+ # mode: Ruby
662
+ # indent-tabs-mode: nil
663
+ # End: