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,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: