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,29 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/clean'
5
+ require 'rake/testtask'
6
+ require 'rdoc/task'
7
+
8
+ Rake::TestTask.new do |task|
9
+ if ((ENV.key? 'RUBY_DEBUG') && (! ENV['RUBY_DEBUG'].empty?)) then
10
+ task.ruby_opts << '-d'
11
+ end
12
+ end
13
+
14
+ Rake::RDocTask.new do |rd|
15
+ rd.rdoc_files.include('lib/**/*.rb')
16
+ end
17
+
18
+ desc 'Build README.html from markdown source.'
19
+ task :readme => %w[ README.html ]
20
+
21
+ file 'README.html' => [ 'README.md' ] do
22
+ sh "pandoc --from=markdown --to=html5 --standalone --self-contained --css=$HOME/.pandoc/github.css --output=README.html README.md"
23
+ end
24
+ CLOBBER.include 'README.html'
25
+
26
+ # Local Variables:
27
+ # mode: Ruby
28
+ # indent-tabs-mode: nil
29
+ # End:
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rims'
4
+
5
+ status = RIMS::Cmd.run_cmd(ARGV)
6
+ exit(status)
7
+
8
+ # Local Variables:
9
+ # mode: Ruby
10
+ # indent-tabs-mode: nil
11
+ # End:
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "rims/version"
4
+
5
+ module RIMS
6
+ autoload :Authentication, 'rims/auth'
7
+ autoload :BufferedWriter, 'rims/server'
8
+ autoload :Checksum_KeyValueStore, 'rims/cksum_kvs'
9
+ autoload :Cmd, 'rims/cmd'
10
+ autoload :Config, 'rims/server'
11
+ autoload :DB, 'rims/db'
12
+ autoload :Daemon, 'rims/daemon'
13
+ autoload :Error, 'rims/error'
14
+ autoload :GDBM_KeyValueStore, 'rims/gdbm_kvs'
15
+ autoload :GlobalDB, 'rims/db'
16
+ autoload :Hash_KeyValueStore, 'rims/hash_kvs'
17
+ autoload :IllegalLockError, 'rims/lock'
18
+ autoload :KeyValueStore, 'rims/kvs'
19
+ autoload :LockError, 'rims/lock'
20
+ autoload :MailFolder, 'rims/mail_store'
21
+ autoload :MailStore, 'rims/mail_store'
22
+ autoload :MailStoreHolder, 'rims/mail_store'
23
+ autoload :MailboxDB, 'rims/db'
24
+ autoload :MessageDB, 'rims/db'
25
+ autoload :MessageSetSyntaxError, 'rims/protocol'
26
+ autoload :Multiplexor, 'rims/server'
27
+ autoload :ObjectPool, 'rims/pool'
28
+ autoload :Password, 'rims/passwd'
29
+ autoload :Protocol, 'rims/protocol'
30
+ autoload :ProtocolError, 'rims/protocol'
31
+ autoload :RFC822, 'rims/rfc822'
32
+ autoload :ReadLockError, 'rims/lock'
33
+ autoload :ReadLockTimeoutError, 'rims/lock'
34
+ autoload :ReadWriteLock, 'rims/lock'
35
+ autoload :Server, 'rims/server'
36
+ autoload :SyntaxError, 'rims/protocol'
37
+ autoload :Test, 'rims/test'
38
+ autoload :WriteLockError, 'rims/lock'
39
+ autoload :WriteLockTimeoutError, 'rims/lock'
40
+ end
41
+
42
+ # Local Variables:
43
+ # mode: Ruby
44
+ # indent-tabs-mode: nil
45
+ # End:
@@ -0,0 +1,133 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'digest'
4
+ require 'openssl'
5
+ require 'securerandom'
6
+
7
+ module RIMS
8
+ class Authentication
9
+ PLUG_IN = {} # :nodoc:
10
+
11
+ class << self
12
+ def unique_user_id(username)
13
+ Digest::SHA256.hexdigest(username).freeze
14
+ end
15
+
16
+ def make_time_source
17
+ proc{ Time.now }
18
+ end
19
+
20
+ def make_random_string_source
21
+ proc{ SecureRandom.uuid }
22
+ end
23
+
24
+ def cram_md5_server_challenge_data(hostname, time_source, random_string_source)
25
+ s = random_string_source.call
26
+ t = time_source.call
27
+ "#{s}.#{t.to_i}@#{hostname}"
28
+ end
29
+
30
+ def hmac_md5_hexdigest(key, data)
31
+ OpenSSL::HMAC.hexdigest('md5', key, data)
32
+ end
33
+
34
+ def add_plug_in(name, klass)
35
+ PLUG_IN[name] = klass
36
+ self
37
+ end
38
+
39
+ def get_plug_in(name, config)
40
+ klass = PLUG_IN[name] or raise KeyError, "not found a password source plug-in: #{name}"
41
+ klass.build_from_conf(config)
42
+ end
43
+ end
44
+
45
+ def initialize(hostname: 'rims',
46
+ time_source: Authentication.make_time_source,
47
+ random_string_source: Authentication.make_random_string_source)
48
+ @hostname = hostname
49
+ @time_source = time_source
50
+ @random_string_source = random_string_source
51
+ @capability = %w[ PLAIN CRAM-MD5 ]
52
+ @plain_src = Password::PlainSource.new
53
+ @passwd_src_list = [ @plain_src ]
54
+ end
55
+
56
+ attr_reader :hostname
57
+ attr_reader :capability
58
+
59
+ def add_plug_in(passwd_src)
60
+ unless (passwd_src.raw_password?) then
61
+ @capability.delete('CRAM-MD5')
62
+ end
63
+ @passwd_src_list << passwd_src
64
+ self
65
+ end
66
+
67
+ def start_plug_in(logger)
68
+ for passwd_src in @passwd_src_list
69
+ logger.info("start password source plug-in: #{passwd_src.class}")
70
+ passwd_src.logger = logger
71
+ passwd_src.start
72
+ end
73
+ end
74
+
75
+ def stop_plug_in(logger)
76
+ for passwd_src in @passwd_src_list.reverse
77
+ logger.info("stop password source plug-in: #{passwd_src.class}")
78
+ passwd_src.stop
79
+ end
80
+ end
81
+
82
+ def entry(username, password)
83
+ @plain_src.entry(username, password)
84
+ self
85
+ end
86
+
87
+ def user?(username)
88
+ @passwd_src_list.any?{|passwd_src| passwd_src.user? username }
89
+ end
90
+
91
+ def authenticate_login(username, password)
92
+ for passwd_src in @passwd_src_list
93
+ if (passwd_src.compare_password(username, password)) then
94
+ return username
95
+ end
96
+ end
97
+
98
+ nil
99
+ end
100
+
101
+ def authenticate_plain(client_response_data)
102
+ authz_id, authc_id, password = client_response_data.split("\0", 3)
103
+ if (authz_id.empty? || (authz_id == authc_id)) then
104
+ authenticate_login(authc_id, password)
105
+ end
106
+ end
107
+
108
+ def cram_md5_server_challenge_data
109
+ self.class.cram_md5_server_challenge_data(@hostname, @time_source, @random_string_source)
110
+ end
111
+
112
+ def authenticate_cram_md5(server_challenge_data, client_response_data)
113
+ username, client_hmac_result_data = client_response_data.split(' ', 2)
114
+ for passwd_src in @passwd_src_list
115
+ if (passwd_src.raw_password?) then
116
+ if (key = passwd_src.fetch_password(username)) then
117
+ server_hmac_result_data = Authentication.hmac_md5_hexdigest(key, server_challenge_data)
118
+ if (client_hmac_result_data == server_hmac_result_data) then
119
+ return username
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ nil
126
+ end
127
+ end
128
+ end
129
+
130
+ # Local Variables:
131
+ # mode: Ruby
132
+ # indent-tabs-mode: nil
133
+ # End:
@@ -0,0 +1,68 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'digest'
4
+
5
+ module RIMS
6
+ class Checksum_KeyValueStore < KeyValueStore
7
+ def initialize(kvs)
8
+ @kvs = kvs
9
+ end
10
+
11
+ def md5_cksum_parse(key, s)
12
+ if (s) then
13
+ s =~ /\Amd5 (\S+?)\n/ or raise "checksum format error at key: #{key}"
14
+ md5_cksum = $1
15
+ value = $'
16
+ if (Digest::MD5.hexdigest(value) != md5_cksum) then
17
+ raise "checksum error at key: #{key}"
18
+ end
19
+
20
+ value
21
+ end
22
+ end
23
+ private :md5_cksum_parse
24
+
25
+ def [](key)
26
+ md5_cksum_parse(key, @kvs[key])
27
+ end
28
+
29
+ def []=(key, value)
30
+ @kvs[key] = "md5 #{Digest::MD5.hexdigest(value)}\n#{value}"
31
+ value
32
+ end
33
+
34
+ def delete(key)
35
+ md5_cksum_parse(key, @kvs.delete(key))
36
+ end
37
+
38
+ def key?(key)
39
+ @kvs.key? key
40
+ end
41
+
42
+ def each_key(&block)
43
+ return enum_for(:each_key) unless block_given?
44
+ @kvs.each_key(&block)
45
+ self
46
+ end
47
+
48
+ def sync
49
+ @kvs.sync
50
+ self
51
+ end
52
+
53
+ def close
54
+ @kvs.close
55
+ self
56
+ end
57
+
58
+ def destroy
59
+ @kvs.destroy
60
+ nil
61
+ end
62
+ end
63
+ end
64
+
65
+ # Local Variables:
66
+ # mode: Ruby
67
+ # indent-tabs-mode: nil
68
+ # End:
@@ -0,0 +1,809 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'logger'
4
+ require 'net/imap'
5
+ require 'optparse'
6
+ require 'pp'if $DEBUG
7
+ require 'syslog'
8
+ require 'syslog/logger'
9
+ require 'yaml'
10
+
11
+ module RIMS
12
+ module Cmd
13
+ CMDs = {}
14
+
15
+ def self.command_function(method_name, description)
16
+ module_function(method_name)
17
+ method_name = method_name.to_s
18
+ unless (method_name =~ /^cmd_/) then
19
+ raise "invalid command function name: #{method_name}"
20
+ end
21
+ cmd_name = $'.gsub(/_/, '-')
22
+ CMDs[cmd_name] = { function: method_name.to_sym, description: description }
23
+ end
24
+
25
+ def run_cmd(args)
26
+ options = OptionParser.new
27
+ if (args.empty?) then
28
+ cmd_help(options, args)
29
+ return 1
30
+ end
31
+
32
+ cmd_name = args.shift
33
+ pp cmd_name if $DEBUG
34
+ pp args if $DEBUG
35
+
36
+ cmd_entry = CMDs[cmd_name] or raise "unknown command: #{cmd_name}. Run `#{options.program_name} help'."
37
+ options.program_name += " #{cmd_name}"
38
+ send(cmd_entry[:function], options, args)
39
+ end
40
+ module_function :run_cmd
41
+
42
+ def cmd_help(options, args)
43
+ show_debug_command = false
44
+ options.on('--show-debug-command', 'Show command for debug in help message. At default, debug command is hidden.') do
45
+ show_debug_command = true
46
+ end
47
+ options.parse!(args)
48
+
49
+ STDERR.puts "usage: #{File.basename($0)} command options"
50
+ STDERR.puts ""
51
+ STDERR.puts "commands:"
52
+ w = CMDs.keys.map{|k| k.length }.max + 4
53
+ fmt = " %- #{w}s%s"
54
+ CMDs.each do |cmd_name, cmd_entry|
55
+ if ((! show_debug_command) && (cmd_name =~ /^debug/)) then
56
+ next
57
+ end
58
+ STDERR.puts format(fmt, cmd_name, cmd_entry[:description])
59
+ end
60
+ STDERR.puts ""
61
+ STDERR.puts "command help options:"
62
+ STDERR.puts " -h, --help"
63
+ 0
64
+ end
65
+ command_function :cmd_help, "Show this message."
66
+
67
+ def cmd_version(options, args)
68
+ options.parse!(args)
69
+ puts RIMS::VERSION
70
+ 0
71
+ end
72
+ command_function :cmd_version, 'Show software version.'
73
+
74
+ def make_server_config(options)
75
+ conf = RIMS::Config.new
76
+ conf.load(base_dir: Dir.getwd)
77
+
78
+ options.on('-h', '--help', 'Show this message.') do
79
+ puts options
80
+ exit
81
+ end
82
+ options.on('-f', '--config-yaml=CONFIG_FILE',
83
+ "Load optional parameters from CONFIG_FILE.") do |path|
84
+ conf.load_config_yaml(path)
85
+ end
86
+ options.on('-d', '--base-dir=DIR',
87
+ "Directory that places log file, mailbox database, etc. default is current directory.") do |path|
88
+ conf.load(base_dir: path)
89
+ end
90
+ level_list = %w[ debug info warn error fatal ]
91
+ stdout_list = level_list + %w[ quiet ]
92
+ options.on('-v', '--log-stdout=LEVEL', stdout_list,
93
+ "Stdout logging level (#{stdout_list.join(' ')}). default is `info'.") do |level|
94
+ conf.load(log_stdout: level)
95
+ end
96
+ options.on('--log-file=FILE',
97
+ "Name of log file. the directory part preceding file name is ignored. default is `imap.log'.") do |path|
98
+ conf.load(log_file: path)
99
+ end
100
+ options.on('-l', '--log-level=LEVEL', level_list,
101
+ "Logging level (#{level_list.join(' ')}). default is `info'.") do |level|
102
+ conf.load(log_level: level)
103
+ end
104
+ options.on('--log-shift-age=NUMBER', Integer, 'Number of old log files to keep.') do |num|
105
+ conf.load(log_shift_age: num)
106
+ end
107
+ options.on('--log-shift-age-daily', 'Frequency of daily log rotation.') do
108
+ conf.load(log_shift_age: 'daily')
109
+ end
110
+ options.on('--log-shift-age-weekly', 'Frequency of weekly log rotation.') do
111
+ conf.load(log_shift_age: 'weekly')
112
+ end
113
+ options.on('--log-shift-age-monthly', 'Frequency of monthly log rotation.') do
114
+ conf.load(log_shift_age: 'monthly')
115
+ end
116
+ options.on('--log-shift-size=SIZE', Integer, 'Maximum logfile size.') do |size|
117
+ conf.load(log_shift_size: size)
118
+ end
119
+ options.on('--kvs-type=TYPE', %w[ gdbm ],
120
+ "Choose the key-value store type of mailbox database. load plug-in on config.yml.") do |type|
121
+ conf.load(key_value_store_type: type)
122
+ end
123
+ options.on('--[no-]use-kvs-cksum',
124
+ "Enable/disable data checksum at key-value store. default is enabled.") do |use|
125
+ conf.load(use_key_value_store_checksum: use)
126
+ end
127
+ options.on('-u', '--username=NAME',
128
+ "Username to login IMAP server. required parameter to start server.") do |name|
129
+ conf.load(username: name)
130
+ end
131
+ options.on('-w', '--password=PASS',
132
+ "Password to login IMAP server. required parameter to start server.") do |pass|
133
+ conf.load(password: pass)
134
+ end
135
+ options.on('--imap-host=HOSTNAME',
136
+ "IMAP server hostname or IP address for the server to bind. default is `#{Server::DEFAULT[:imap_host]}'.") do |host|
137
+ conf.load(imap_host: host)
138
+ end
139
+ options.on('--imap-port=PORT',
140
+ "IMAP server port number or service name for the server to bind. default is `#{Server::DEFAULT[:imap_port]}'.") do |value|
141
+ if (value =~ /\A\d+\z/) then
142
+ port_number = value.to_i
143
+ conf.load(imap_port: port_number)
144
+ else
145
+ service_name = value
146
+ conf.load(imap_port: service_name)
147
+ end
148
+ end
149
+ options.on('--privilege-user=NAME',
150
+ "Privilege user name or ID for server process. default is #{Server::DEFAULT[:process_privilege_uid]}.") do |name|
151
+ conf.load(process_privilege_user: name)
152
+ end
153
+ options.on('--privilege-group=NAME',
154
+ "Privilege group name or ID for server process. default is #{Server::DEFAULT[:process_privilege_gid]}.") do |name|
155
+ conf.load(process_privilege_user: name)
156
+ end
157
+
158
+ options.on('--ip-addr=IP_ADDR', 'obsoleted.') do |ip_addr|
159
+ warn("warning: `--ip-addr=IP_ADDR' is obsoleted option and should use `--imap-host=HOSTNAME'.")
160
+ conf.load(ip_addr: ip_addr)
161
+ end
162
+ options.on('--ip-port=PORT', Integer, 'obsoleted.') do |port|
163
+ warn("warning: `--ip-port=PORT' is obsoleted option and should use `--imap-port=PORT'.")
164
+ conf.load(ip_port: port)
165
+ end
166
+
167
+ conf
168
+ end
169
+ module_function :make_server_config
170
+
171
+ def cmd_server(options, args)
172
+ conf = make_server_config(options)
173
+ options.parse!(args)
174
+
175
+ server = conf.build_server
176
+ server.start
177
+
178
+ 0
179
+ end
180
+ command_function :cmd_server, "Run IMAP server."
181
+
182
+ def imap_res2str(imap_response)
183
+ "#{imap_response.name} #{imap_response.data.text}"
184
+ end
185
+ module_function :imap_res2str
186
+
187
+ class Config
188
+ def imap_res2str(imap_response)
189
+ Cmd.imap_res2str(imap_response)
190
+ end
191
+ private :imap_res2str
192
+
193
+ IMAP_AUTH_TYPE_LIST = %w[ login plain cram-md5 ]
194
+ MAIL_DATE_PLACE_LIST = [ :servertime, :localtime, :filetime, :mailheader ]
195
+
196
+ VERBOSE_OPTION_LIST = [
197
+ [ :verbose, false, '-v', '--[no-]verbose', "Enable verbose messages. default is no verbose." ]
198
+ ]
199
+
200
+ def self.make_imap_connect_option_list(imap_host: 'localhost', imap_port: 143, imap_ssl: false, auth_type: 'login', username: nil)
201
+ [ [ :imap_host, imap_host, '-n', '--host=HOSTNAME', "Hostname or IP address to connect IMAP server. default is `#{imap_host}'." ],
202
+ [ :imap_port, imap_port, '-o', '--port=PORT', Integer, "Server port number or service name to connect IMAP server. default is #{imap_port}." ],
203
+ [ :imap_ssl, imap_ssl, '-s', '--[no-]use-ssl', "Enable SSL/TLS connection. default is #{imap_ssl ? 'enabled' : 'disabled'}." ],
204
+ [ :username, username, '-u', '--username=NAME',
205
+ "Username to login IMAP server. " + if (username) then
206
+ "default is `#{username}'."
207
+ else
208
+ "required parameter to connect server."
209
+ end ],
210
+ [ :password, nil, '-w', '--password=PASS', "Password to login IMAP server. required parameter to connect server." ],
211
+ [ :auth_type, auth_type, '--auth-type=METHOD', IMAP_AUTH_TYPE_LIST,
212
+ "Choose authentication method type (#{IMAP_AUTH_TYPE_LIST.join(' ')}). default is `#{auth_type}'." ]
213
+ ]
214
+ end
215
+
216
+ IMAP_CONNECT_OPTION_LIST = self.make_imap_connect_option_list
217
+ POST_MAIL_CONNECT_OPTION_LIST = self.make_imap_connect_option_list(imap_port: Server::DEFAULT[:imap_port],
218
+ username: Server::DEFAULT[:mail_delivery_user])
219
+
220
+ IMAP_MAILBOX_OPTION_LIST = [
221
+ [ :mailbox, 'INBOX', '-m', '--mailbox=NAME', "Set mailbox name to append messages. default is `INBOX'." ]
222
+ ]
223
+
224
+ IMAP_STORE_FLAG_OPTION_LIST = [
225
+ [ :store_flag_answered, false, '--[no-]store-flag-answered', "Store answered flag on appending messages to mailbox. default is no flag." ],
226
+ [ :store_flag_flagged, false, '--[no-]store-flag-flagged', "Store flagged flag on appending messages to mailbox. default is no flag." ],
227
+ [ :store_flag_deleted, false, '--[no-]store-flag-deleted', "Store deleted flag on appending messages to mailbox. default is no flag." ],
228
+ [ :store_flag_seen, false, '--[no-]store-flag-seen', "Store seen flag on appending messages to mailbox. default is no flag." ],
229
+ [ :store_flag_draft, false, '--[no-]store-flag-draft', "Store draft flag on appending messages to mailbox. default is no flag." ]
230
+ ]
231
+
232
+ MAIL_DATE_OPTION_LIST = [
233
+ [ :look_for_date, :servertime, '--look-for-date=PLACE', MAIL_DATE_PLACE_LIST,
234
+ "Choose the place (#{MAIL_DATE_PLACE_LIST.join(' ')}) to look for the date that as internaldate is appended with message. default is `servertime'."
235
+ ]
236
+ ]
237
+
238
+ def initialize(options, option_list)
239
+ @options = options
240
+ @option_list = option_list
241
+ @conf = {}
242
+ for key, value, *option_description in option_list
243
+ @conf[key] = value
244
+ end
245
+ end
246
+
247
+ def [](key)
248
+ @conf[key]
249
+ end
250
+
251
+ def setup_option_list
252
+ @option_list.each do |key, value, *option_description|
253
+ @options.on(*option_description) do |v|
254
+ @conf[key] = v
255
+ end
256
+ end
257
+
258
+ self
259
+ end
260
+
261
+ def help_option(add_banner: nil)
262
+ @options.banner += add_banner if add_banner
263
+ @options.on('-h', '--help', 'Show this message.') do
264
+ puts @options
265
+ exit
266
+ end
267
+
268
+ self
269
+ end
270
+
271
+ def quiet_option(default_verbose: true)
272
+ @conf[:verbose] = default_verbose
273
+ @options.on('-v', '--[no-]verbose', 'Enable verbose messages. default is verbose.') do |verbose|
274
+ @conf[:verbose] = verbose
275
+ end
276
+ @options.on('-q', '--[no-]quiet', 'Disable verbose messages. default is verbose.') do |quiet|
277
+ @conf[:verbose] = ! quiet
278
+ end
279
+
280
+ self
281
+ end
282
+
283
+ def load_config_option
284
+ @options.on('-f', '--config-yaml=CONFIG_FILE',
285
+ "Load optional parameters from CONFIG_FILE.") do |path|
286
+ for name, value in YAML.load_file(path)
287
+ @conf[name.to_sym] = value
288
+ end
289
+ end
290
+
291
+ self
292
+ end
293
+
294
+ def load_library_option
295
+ @options.on('-r', '--load-library=LIBRARY', 'require LIBRARY.') do |library|
296
+ require(library)
297
+ end
298
+
299
+ self
300
+ end
301
+
302
+ def key_value_store_option
303
+ @conf[:key_value_store_type] = GDBM_KeyValueStore
304
+ @options.on('--kvs-type=TYPE', 'Choose the key-value store type.') do |kvs_type|
305
+ @conf[:key_value_store_type] = KeyValueStore::FactoryBuilder.get_plug_in(kvs_type)
306
+ end
307
+
308
+ @conf[:use_key_value_store_checksum] = true
309
+ @options.on('--[no-]use-kvs-cksum', 'Enable/disable data checksum at key-value store. default is enabled.') do |use_checksum|
310
+ @conf[:use_key_value_store_checksum] = use_checksum
311
+ end
312
+
313
+ self
314
+ end
315
+
316
+ def parse_options!(args, order: false)
317
+ if (order) then
318
+ @options.order!(args)
319
+ else
320
+ @options.parse!(args)
321
+ end
322
+ pp @conf if $DEBUG
323
+
324
+ self
325
+ end
326
+
327
+ def imap_debug_option
328
+ @options.on('--[no-]imap-debug',
329
+ "Set the debug flag of Net::IMAP class. default is false.") do |v|
330
+ Net::IMAP.debug = v
331
+ end
332
+
333
+ self
334
+ end
335
+
336
+ def imap_connect
337
+ unless (@conf[:username] && @conf[:password]) then
338
+ raise 'need for username and password.'
339
+ end
340
+
341
+ imap = Net::IMAP.new(@conf[:imap_host], port: @conf[:imap_port], ssl: @conf[:imap_ssl])
342
+ begin
343
+ if (@conf[:verbose]) then
344
+ puts "server greeting: #{imap_res2str(imap.greeting)}"
345
+ puts "server capability: #{imap.capability.join(' ')}"
346
+ end
347
+
348
+ case (@conf[:auth_type])
349
+ when 'login'
350
+ res = imap.login(@conf[:username], @conf[:password])
351
+ puts "login: #{imap_res2str(res)}" if @conf[:verbose]
352
+ when 'plain', 'cram-md5'
353
+ res = imap.authenticate(@conf[:auth_type], @conf[:username], @conf[:password])
354
+ puts "authenticate: #{imap_res2str(res)}" if @conf[:verbose]
355
+ else
356
+ raise "unknown authentication type: #{@conf[:auth_type]}"
357
+ end
358
+
359
+ yield(imap)
360
+ ensure
361
+ Error.suppress_2nd_error_at_resource_closing{ imap.logout }
362
+ end
363
+ end
364
+
365
+ def make_imap_store_flags
366
+ store_flags = []
367
+ [ [ :store_flag_answered, :Answered ],
368
+ [ :store_flag_flagged, :Flagged ],
369
+ [ :store_flag_deleted, :Deleted ],
370
+ [ :store_flag_seen, :Seen ],
371
+ [ :store_flag_draft, :Draft ]
372
+ ].each do |key, flag|
373
+ if (@conf[key]) then
374
+ store_flags << flag
375
+ end
376
+ end
377
+ puts "store flags: (#{store_flags.join(' ')})" if @conf[:verbose]
378
+
379
+ store_flags
380
+ end
381
+
382
+ def look_for_date(message_text, path=nil)
383
+ case (@conf[:look_for_date])
384
+ when :servertime
385
+ nil
386
+ when :localtime
387
+ Time.now
388
+ when :filetime
389
+ if (path) then
390
+ File.stat(path).mtime
391
+ end
392
+ when :mailheader
393
+ RFC822::Message.new(message_text).date
394
+ else
395
+ raise "failed to look for date: #{place}"
396
+ end
397
+ end
398
+
399
+ def make_kvs_factory
400
+ builder = KeyValueStore::FactoryBuilder.new
401
+ builder.open{|name| @conf[:key_value_store_type].open_with_conf(name, {}) }
402
+ if (@conf[:use_key_value_store_checksum]) then
403
+ builder.use(Checksum_KeyValueStore)
404
+ end
405
+ builder.factory
406
+ end
407
+ end
408
+
409
+ def cmd_daemon(options, args)
410
+ conf = Config.new(options,
411
+ [ [ :is_daemon,
412
+ true,
413
+ '--[no-]daemon',
414
+ 'Start daemon process. default is enabled.'
415
+ ],
416
+ [ :is_syslog,
417
+ true,
418
+ '--[no-]syslog',
419
+ 'Syslog daemon messages. default is enabled.'
420
+ ]
421
+ ])
422
+ conf.help_option(add_banner: ' start/stop/restart/status [server options]')
423
+ conf.quiet_option
424
+ conf.setup_option_list
425
+ conf.parse_options!(args, order: true)
426
+ pp args if $DEBUG
427
+
428
+ operation = args.shift or raise 'need for daemon operation.'
429
+ server_args = args.dup
430
+ server_options = OptionParser.new
431
+ server_conf = make_server_config(server_options)
432
+ server_options.parse!(server_args)
433
+ stat_file_path = Daemon.make_stat_file_path(server_conf.base_dir)
434
+ pp server_conf if $DEBUG
435
+
436
+ case (operation)
437
+ when 'start'
438
+ if (conf[:is_daemon]) then
439
+ args += %w[ --log-stdout=quiet ]
440
+ Process.daemon(true)
441
+ end
442
+
443
+ logger = Multiplexor.new
444
+ unless (conf[:is_daemon]) then
445
+ stdout_logger = Logger.new(STDOUT)
446
+ def stdout_logger.close # should not be closed at child process.
447
+ nil
448
+ end
449
+ logger.add(stdout_logger)
450
+ end
451
+ if (conf[:is_syslog]) then
452
+ syslog_logger = Syslog::Logger.new('rims-daemon')
453
+ def syslog_logger.close # should be closed at child process.
454
+ Syslog.close
455
+ end
456
+ logger.add(syslog_logger)
457
+ end
458
+
459
+ daemon = Daemon.new(stat_file_path, logger, server_options: args)
460
+
461
+ [ [ Daemon::RELOAD_SIGNAL_LIST, proc{ daemon.reload_server } ],
462
+ [ Daemon::RESTART_SIGNAL_LIST, proc{ daemon.restart_server } ],
463
+ [ Daemon::STOP_SIGNAL_LIST, proc{ daemon.stop_server } ]
464
+ ].each do |signal_list, signal_command|
465
+ for sig_name in signal_list
466
+ Signal.trap(sig_name, signal_command)
467
+ end
468
+ end
469
+
470
+ daemon.run
471
+ when 'stop'
472
+ stat_file = Daemon.new_status_file(stat_file_path)
473
+ stat_file.open{
474
+ stat_file.should_be_locked
475
+ pid = YAML.load(stat_file.read)['pid']
476
+ Process.kill(Daemon::STOP_SIGNAL, pid)
477
+ }
478
+ when 'restart'
479
+ stat_file = Daemon.new_status_file(stat_file_path)
480
+ stat_file.open{
481
+ stat_file.should_be_locked
482
+ pid = YAML.load(stat_file.read)['pid']
483
+ Process.kill(Daemon::RESTART_SIGNAL, pid)
484
+ }
485
+ when 'status'
486
+ stat_file = Daemon.new_status_file(stat_file_path)
487
+ stat_file.open{
488
+ if (stat_file.locked?) then
489
+ puts 'daemon is running.' if conf[:verbose]
490
+ return 0
491
+ else
492
+ puts 'daemon is stopped.' if conf[:verbose]
493
+ return 1
494
+ end
495
+ }
496
+ else
497
+ raise "unknown daemon operation: #{operation}"
498
+ end
499
+
500
+ 0
501
+ end
502
+ command_function :cmd_daemon, "Daemon start/stop/status tool."
503
+
504
+ def imap_append(imap, mailbox, message, store_flags: [], date_time: nil, verbose: false)
505
+ puts "message date: #{date_time}" if (verbose && date_time)
506
+ store_flags = nil if store_flags.empty?
507
+ res = imap.append(mailbox, message, store_flags, date_time)
508
+ puts "append: #{imap_res2str(res)}" if verbose
509
+ nil
510
+ end
511
+ module_function :imap_append
512
+
513
+ def each_message(args, verbose: false)
514
+ if (args.empty?) then
515
+ msg_txt = STDIN.read
516
+ yield(msg_txt)
517
+ return 0
518
+ else
519
+ error_count = 0
520
+ args.each_with_index do |filename, i|
521
+ puts "progress: #{i + 1}/#{args.length}" if verbose
522
+ begin
523
+ msg_txt = IO.read(filename, mode: 'rb', encoding: 'ascii-8bit')
524
+ yield(msg_txt)
525
+ rescue
526
+ error_count += 1
527
+ puts "failed to append message: #{filename}"
528
+ puts "error: #{$!}"
529
+ if ($DEBUG) then
530
+ for frame in $!.backtrace
531
+ puts frame
532
+ end
533
+ end
534
+ end
535
+ end
536
+
537
+ if (error_count > 0) then
538
+ puts "#{error_count} errors!"
539
+ return 1
540
+ else
541
+ return 0
542
+ end
543
+ end
544
+ end
545
+ module_function :each_message
546
+
547
+ def cmd_post_mail(options, args)
548
+ STDIN.set_encoding(Encoding::ASCII_8BIT)
549
+
550
+ option_list =
551
+ Config::VERBOSE_OPTION_LIST +
552
+ Config::POST_MAIL_CONNECT_OPTION_LIST +
553
+ Config::IMAP_MAILBOX_OPTION_LIST +
554
+ Config::IMAP_STORE_FLAG_OPTION_LIST +
555
+ Config::MAIL_DATE_OPTION_LIST
556
+
557
+ conf = Config.new(options, option_list)
558
+ conf.help_option(add_banner: ' [POST USER] [MESSAGE_FILEs]')
559
+ conf.load_config_option
560
+ conf.setup_option_list
561
+ conf.imap_debug_option
562
+ conf.parse_options!(args)
563
+
564
+ post_user = args.shift or raise 'need for post user.'
565
+
566
+ store_flags = conf.make_imap_store_flags
567
+ conf.imap_connect{|imap|
568
+ unless (imap.capability.find{|c| c == 'X-RIMS-MAIL-DELIVERY-USER' }) then
569
+ warn('warning: This IMAP server might not support RIMS mail delivery protocol.')
570
+ end
571
+ each_message(args) do |msg_txt|
572
+ t = conf.look_for_date(msg_txt)
573
+ encoded_mbox_name = Protocol::Decoder.encode_delivery_target_mailbox(post_user, conf[:mailbox])
574
+ imap_append(imap, encoded_mbox_name, msg_txt, store_flags: store_flags, date_time: t, verbose: conf[:verbose])
575
+ end
576
+ }
577
+ end
578
+ command_function :cmd_post_mail, "Post mail to any user."
579
+
580
+ def cmd_imap_append(options, args)
581
+ STDIN.set_encoding(Encoding::ASCII_8BIT)
582
+
583
+ option_list =
584
+ Config::VERBOSE_OPTION_LIST +
585
+ Config::IMAP_CONNECT_OPTION_LIST +
586
+ Config::IMAP_MAILBOX_OPTION_LIST +
587
+ Config::IMAP_STORE_FLAG_OPTION_LIST +
588
+ Config::MAIL_DATE_OPTION_LIST
589
+
590
+ conf = Config.new(options, option_list)
591
+ conf.help_option(add_banner: ' [MESSAGE_FILEs]')
592
+ conf.load_config_option
593
+ conf.setup_option_list
594
+ conf.imap_debug_option
595
+ conf.parse_options!(args)
596
+
597
+ store_flags = conf.make_imap_store_flags
598
+ conf.imap_connect{|imap|
599
+ each_message(args) do |msg_txt|
600
+ t = conf.look_for_date(msg_txt)
601
+ imap_append(imap, conf[:mailbox], msg_txt, store_flags: store_flags, date_time: t, verbose: conf[:verbose])
602
+ end
603
+ }
604
+ end
605
+ command_function :cmd_imap_append, "Append message to IMAP mailbox."
606
+
607
+ def cmd_mbox_dirty_flag(options, args)
608
+ option_list = [
609
+ [ :return_flag_exit_code, true, '--[no-]return-flag-exit-code', 'Dirty flag value is returned to exit code. default is true.' ]
610
+ ]
611
+
612
+ conf = Config.new(options, option_list)
613
+ conf.load_library_option
614
+ conf.key_value_store_option
615
+ conf.help_option(add_banner: ' [mailbox directory]')
616
+ conf.quiet_option
617
+ conf.setup_option_list
618
+
619
+ write_dirty_flag = nil
620
+ options.on('--enable-dirty-flag', 'Enable mailbox dirty flag.') { write_dirty_flag = true }
621
+ options.on('--disable-dirty-flag', 'Disable mailbox dirty flag.') { write_dirty_flag = false }
622
+
623
+ conf.parse_options!(args)
624
+ pp conf if $DEBUG
625
+
626
+ mbox_dir = args.shift or raise 'need for mailbox directory.'
627
+ meta_db_path = File.join(mbox_dir, 'meta')
628
+ unless (conf[:key_value_store_type].exist? meta_db_path) then
629
+ raise "not found a mailbox meta DB: #{meta_db_path}"
630
+ end
631
+
632
+ kvs_factory = conf.make_kvs_factory
633
+ meta_db = DB::Meta.new(kvs_factory.call(File.join(mbox_dir, 'meta')))
634
+ begin
635
+ unless (write_dirty_flag.nil?) then
636
+ meta_db.dirty = write_dirty_flag
637
+ end
638
+
639
+ if (conf[:verbose]) then
640
+ puts "dirty flag is #{meta_db.dirty?}."
641
+ end
642
+
643
+ if (conf[:return_flag_exit_code]) then
644
+ if (meta_db.dirty?) then
645
+ 1
646
+ else
647
+ 0
648
+ end
649
+ else
650
+ 0
651
+ end
652
+ ensure
653
+ Error.suppress_2nd_error_at_resource_closing{ meta_db.close }
654
+ end
655
+ end
656
+ command_function :cmd_mbox_dirty_flag, 'Show/enable/disable dirty flag of mailbox database.'
657
+
658
+ def cmd_unique_user_id(options, args)
659
+ options.banner += ' [username]'
660
+ options.parse!(args)
661
+
662
+ if (args.length != 1) then
663
+ raise 'need for a username.'
664
+ end
665
+ username = args.shift
666
+
667
+ puts Authentication.unique_user_id(username)
668
+
669
+ 0
670
+ end
671
+ command_function :cmd_unique_user_id, 'Show unique user ID from username.'
672
+
673
+ def cmd_show_user_mbox(options, args)
674
+ conf = RIMS::Config.new
675
+ load_server_config = false
676
+
677
+ options.banner += ' [base directory] [username] OR -f [config.yml path] [username]'
678
+ options.on('-f', '--config-yaml=CONFIG_FILE',
679
+ 'Load optional parameters from CONFIG_FILE.') do |path|
680
+ conf.load_config_yaml(path)
681
+ load_server_config = true
682
+ end
683
+ options.parse!(args)
684
+
685
+ unless (load_server_config) then
686
+ base_dir = args.shift or raise 'need for base directory.'
687
+ conf.load(base_dir: base_dir)
688
+ end
689
+
690
+ username = args.shift or raise 'need for a username.'
691
+ unique_user_id = Authentication.unique_user_id(username)
692
+ puts conf.make_key_value_store_path_from_base_dir(MAILBOX_DATA_STRUCTURE_VERSION, unique_user_id)
693
+
694
+ 0
695
+ end
696
+ command_function :cmd_show_user_mbox, "Show the path in which user's mailbox data is stored."
697
+
698
+ def cmd_pass_hash(options, args)
699
+ option_list = [
700
+ [ :hash_type, 'SHA256', '--hash-type=DIGEST', 'Password hash type (ex SHA256, MD5, etc). default is SHA256.' ],
701
+ [ :stretch_count, 10000, '--stretch-count=COUNT', Integer, 'Count to stretch password hash. default is 10000.' ],
702
+ [ :salt_size, 16, '--salt-size=OCTETS', Integer, 'Size of salt string. default is 16 octets.' ]
703
+ ]
704
+
705
+ conf = Config.new(options, option_list)
706
+ conf.help_option(add_banner: <<-'EOF'.chomp)
707
+ passwd_plain.yml
708
+ Example
709
+ $ cat passwd_plain.yml
710
+ - { user: foo, pass: open_sesame }
711
+ - { user: "#postman", pass: "#postman" }
712
+ $ rims pass-hash passwd_plain.yml >passwd_hash.yml
713
+ $ cat passwd_hash.yml
714
+ ---
715
+ - user: foo
716
+ hash: SHA256:10000:YkslZucwN2QJ7LOft59Pgw==:d5dca9109cc787220eba65810e40165079ce3292407e74e8fbd5c6a8a9b12204
717
+ - user: "#postman"
718
+ hash: SHA256:10000:6Qj/wAYmb7NUGdOy0N35qg==:e967e46b8e0d9df6324e66c7e42da64911a8715e06a123fe5abf7af4ca45a386
719
+ Options:
720
+ EOF
721
+ conf.setup_option_list
722
+ conf.parse_options!(args)
723
+ pp conf if $DEBUG
724
+
725
+ case (args.length)
726
+ when 0
727
+ passwd, *optional = YAML.load_stream(STDIN)
728
+ when 1
729
+ passwd, *optional = File.open(args[0]) {|f| YAML.load_stream(f) }
730
+ else
731
+ raise ArgumentError, 'too many input files.'
732
+ end
733
+
734
+ digest_factory = Password::HashSource.search_digest_factory(conf[:hash_type])
735
+ salt_generator = Password::HashSource.make_salt_generator(conf[:salt_size])
736
+
737
+ for entry in passwd
738
+ pass = entry.delete('pass') or raise "not found a `pass' entry."
739
+ entry['hash'] = Password::HashSource.make_entry(digest_factory, conf[:stretch_count], salt_generator.call, pass).to_s
740
+ end
741
+
742
+ puts passwd.to_yaml
743
+
744
+ 0
745
+ end
746
+ command_function :cmd_pass_hash, 'Make hash password configuration file from plain password configuration file.'
747
+
748
+ def cmd_debug_dump_kvs(options, args)
749
+ option_list = [
750
+ [ :match_key, nil, '--match-key=REGEXP', Regexp, 'Show keys matching regular expression.' ],
751
+ [ :dump_size, true, '--[no-]dump-size', 'Dump size of value with key.' ],
752
+ [ :dump_value, true, '--[no-]dump-value', 'Dump value with key.' ],
753
+ [ :marshal_restore, true, '--[no-]marshal-restore', 'Restore serialized object.' ]
754
+ ]
755
+
756
+ conf = Config.new(options, option_list)
757
+ conf.load_library_option
758
+ conf.key_value_store_option
759
+ conf.help_option(add_banner: ' [DB_NAME]')
760
+ conf.setup_option_list
761
+ conf.parse_options!(args)
762
+ pp conf if $DEBUG
763
+
764
+ name = args.shift or raise 'need for DB name.'
765
+ unless (conf[:key_value_store_type].exist? name) then
766
+ raise "not found a key-value store: #{name}"
767
+ end
768
+
769
+ factory = conf.make_kvs_factory
770
+ db = factory.call(name)
771
+ begin
772
+ db.each_key do |key|
773
+ if (conf[:match_key] && (key !~ conf[:match_key])) then
774
+ next
775
+ end
776
+
777
+ entry = key.inspect
778
+ if (conf[:dump_size]) then
779
+ size = db[key].bytesize
780
+ entry += ": #{size} bytes"
781
+ end
782
+ if (conf[:dump_value]) then
783
+ v = db[key]
784
+ if (conf[:marshal_restore]) then
785
+ begin
786
+ v = Marshal.restore(v)
787
+ rescue
788
+ # not marshal object!
789
+ end
790
+ end
791
+ entry += ": #{v.inspect}"
792
+ end
793
+
794
+ puts entry
795
+ end
796
+ ensure
797
+ Error.suppress_2nd_error_at_resource_closing{ db.close }
798
+ end
799
+
800
+ 0
801
+ end
802
+ command_function :cmd_debug_dump_kvs, "Dump key-value store contents."
803
+ end
804
+ end
805
+
806
+ # Local Variables:
807
+ # mode: Ruby
808
+ # indent-tabs-mode: nil
809
+ # End: