postdb 0.1.5

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.
@@ -0,0 +1,69 @@
1
+ module PostDB
2
+ module CLI
3
+ class Domains < Thor
4
+ class DKIM < Thor
5
+ no_tasks do
6
+ include PostDB::CLI::Helper
7
+ end
8
+
9
+ desc "dns DOMAIN", "Get the DNS record for a domain's DKIM key"
10
+ def dns(domain_name = nil)
11
+ unless domain_name
12
+ domains = PostDB::Domain.all
13
+
14
+ if domains.empty?
15
+ exit_with_warning("There don't appear to be any domains on this system.")
16
+ end
17
+
18
+ domain_name = prompt.select("Domain:", domains.map(&:name))
19
+ end
20
+
21
+ domain_name = domain_name.downcase
22
+
23
+ domain = PostDB::Domain.where(name: domain_name).first
24
+
25
+ dkim_public = [domain.dkim.public_key.to_der].pack('m').gsub("\n", '')
26
+
27
+ puts "mail._domainkey.#{domain.name}:"
28
+ puts " v=DKIM1; k=rsa; p=#{dkim_public};"
29
+ end
30
+
31
+ desc "regenerate DOMAIN", "Regenerate the DKIM key for a domain"
32
+ option :force, type: :boolean, default: false
33
+ def regenerate(domain_name = nil)
34
+ unless domain_name
35
+ domains = PostDB::Domain.all
36
+
37
+ if domains.empty?
38
+ exit_with_warning("There don't appear to be any domains on this system.")
39
+ end
40
+
41
+ domain_name = prompt.select("Domain:", domains.map(&:name))
42
+ end
43
+
44
+ domain_name = domain_name.downcase
45
+
46
+ domains = PostDB::Domain.where(name: domain_name)
47
+
48
+ if domains.empty?
49
+ exit_with_warning("The domain '#{domain_name}' could not be found.")
50
+ end
51
+
52
+ unless options[:force]
53
+ confirm_action!("Regenerate the DKIM key for '#{domain_name}'?", "'#{domain_name}' left untouched.")
54
+ end
55
+
56
+ domains.each do |domain|
57
+ domain.regenerate_dkim
58
+
59
+ unless domain.save
60
+ exit_with_error("The DKIM key for '#{domain_name}' couldn't be regenerated.")
61
+ end
62
+ end
63
+
64
+ prompt.ok("The DKIM key for '#{domain_name}' has been regenerated.")
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,101 @@
1
+ module PostDB
2
+ module CLI
3
+ # The helper CLI module
4
+ #
5
+ # This module contains helper functionality for the CLI.
6
+ #
7
+ module Helper
8
+ # Print a newline character
9
+ #
10
+ # Example:
11
+ # >> new_line
12
+ # => "\n"
13
+ #
14
+ def new_line
15
+ puts ""
16
+ end
17
+
18
+ # Get a prompt object
19
+ #
20
+ # Example:
21
+ # >> prompt
22
+ # => #<TTY::Prompt:0x00000000000000>
23
+ #
24
+ def prompt
25
+ @prompt ||= TTY::Prompt.new
26
+ end
27
+
28
+ # Exit with an error
29
+ #
30
+ # Arguments:
31
+ # messages: (Splat)
32
+ #
33
+ # Example:
34
+ # >> exit_with_error "Oops!"
35
+ # => nil
36
+ #
37
+ def exit_with_error(*messages)
38
+ messages.each do |message|
39
+ ("\n" == message) ? print(message) : prompt.error(message)
40
+ end
41
+
42
+ exit 1
43
+ end
44
+
45
+ # Exit with a warning
46
+ #
47
+ # Arguments:
48
+ # messages: (Splat)
49
+ #
50
+ # Example:
51
+ # >> exit_with_warning "Oops!"
52
+ # => nil
53
+ #
54
+ def exit_with_warning(*messages)
55
+ messages.each do |message|
56
+ ("\n" == message) ? print(message) : prompt.warn(message)
57
+ end
58
+
59
+ exit 1
60
+ end
61
+
62
+ # Confirm an action
63
+ #
64
+ # Arguments:
65
+ # message: (String)
66
+ # aborted_message: (String)
67
+ #
68
+ # Example:
69
+ # >> confirm_action! "Do something?"
70
+ # => nil
71
+ #
72
+ def confirm_action!(message, aborted_message = nil)
73
+ # Prompt the user to confirm the action
74
+ if prompt.no?(message)
75
+ # Exit with a warning
76
+ exit_with_warning(aborted_message)
77
+ end
78
+ end
79
+
80
+ # Format an error
81
+ #
82
+ # Arguments:
83
+ # error: (Exception)
84
+ #
85
+ # Example:
86
+ # >> format_error(error)
87
+ # => ["\n", "Error:", " Error Message", "Backtrace:", " from (irb):1"]
88
+ #
89
+ def format_error(error)
90
+ errors = Array.new
91
+ errors << "\n"
92
+ errors << "Error:"
93
+ errors << " " + error.message
94
+ errors << "\n"
95
+ errors << "Backtrace:"
96
+ errors += error.backtrace.map { |trace| " " + trace }
97
+ errors
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,44 @@
1
+ module PostDB
2
+ module CLI
3
+ # The main CLI class
4
+ #
5
+ # This class contains the core CLI functionality.
6
+ #
7
+ class Main < Thor
8
+ class_option :configuration_file, type: :string, aliases: :c, default: '/etc/postdb/postdb.yml'
9
+ class_option :verbose, type: :boolean, aliases: :v, default: false
10
+
11
+ no_tasks do
12
+ include PostDB::CLI::Helper
13
+
14
+ def initialize(*args, &block)
15
+ super(*args, &block)
16
+
17
+ configuration_file = options[:configuration_file]
18
+
19
+ unless File.file?(configuration_file)
20
+ exit_with_error("The configuration file '#{configuration_file}' is missing.")
21
+ end
22
+
23
+ begin
24
+ PostDB.setup(configuration_file)
25
+ rescue PostDB::SetupError => e
26
+ exit_with_error(e.message)
27
+ end
28
+ end
29
+ end
30
+
31
+ desc "domains SUBCOMMAND ...ARGS", "Manage the domains"
32
+ subcommand "domains", PostDB::CLI::Domains
33
+
34
+ desc "users SUBCOMMAND ...ARGS", "Manage the users"
35
+ subcommand "users", PostDB::CLI::Users
36
+
37
+ desc "aliases SUBCOMMAND ...ARGS", "Manage the aliases"
38
+ subcommand "aliases", PostDB::CLI::Aliases
39
+
40
+ desc "database SUBCOMMAND ...ARGS", "Manage the database"
41
+ subcommand "database", PostDB::CLI::Database
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,146 @@
1
+ module PostDB
2
+ module CLI
3
+ class Users < Thor
4
+ no_tasks do
5
+ include PostDB::CLI::Helper
6
+
7
+ def get_password_or_ask
8
+ password = options[:password]
9
+
10
+ unless password
11
+ password = prompt.mask("Password:") do |q|
12
+ q.required(true)
13
+ end
14
+
15
+ password_confirmation = prompt.mask("Password Confirmation:") do |q|
16
+ q.required(true)
17
+ end
18
+
19
+ unless password == password_confirmation
20
+ exit_with_error("Those passwords don't match.")
21
+ end
22
+ end
23
+
24
+ password
25
+ end
26
+ end
27
+
28
+ desc "list DOMAIN", "List all users"
29
+ def list(domain = nil)
30
+ domains = PostDB::Domain.where(**(domain ? { name: domain } : {}))
31
+
32
+ if domains.empty?
33
+ if domain
34
+ exit_with_warning("The domain '#{domain}' could not be found.")
35
+ else
36
+ exit_with_warning("There don't appear to be any domains on this system.")
37
+ end
38
+ end
39
+
40
+ domains.each_with_index do |domain, index|
41
+ users = domain.users.sort { |a, b| a.email <=> b.email }
42
+
43
+ puts TTY::Table.new(
44
+ header: [domain.name].pad(' '),
45
+ rows: users.empty? ? [[' No Users ']] : users.map { |u| [u.email].pad(' ') }
46
+ ).render(:ascii)
47
+
48
+ new_line unless (index + 1) == domains.count
49
+ end
50
+ end
51
+
52
+ desc "add EMAIL", "Add a user"
53
+ option :password, type: :string, default: nil, aliases: :p
54
+ def add(email = nil)
55
+ unless email
56
+ email = prompt.ask("Email:") do |q|
57
+ q.required(true)
58
+ q.validate(:email)
59
+ end
60
+ end
61
+
62
+ email = email.downcase
63
+
64
+ if PostDB::User.where(email: email).count > 0
65
+ exit_with_warning("The user '#{email}' has already been added.")
66
+ end
67
+
68
+ domain_name = email.split('@')[1]
69
+
70
+ unless domain = PostDB::Domain.where(name: domain_name).first
71
+ exit_with_error("The domain '#{domain_name}' is not available.")
72
+ end
73
+
74
+ password = get_password_or_ask
75
+
76
+ PostDB::User.create(domain: domain, email: email, password: password)
77
+
78
+ prompt.ok("The user '#{email}' has been added.")
79
+ end
80
+
81
+ desc "remove EMAIL", "Remove a user"
82
+ option :force, type: :boolean, default: false
83
+ def remove(email = nil)
84
+ unless email
85
+ users = PostDB::User.all
86
+
87
+ if users.empty?
88
+ exit_with_warning("There don't appear to be any users on this system.")
89
+ end
90
+
91
+ email = prompt.select("User:", users.map(&:email))
92
+ end
93
+
94
+ email = email.downcase
95
+
96
+ users = PostDB::User.where(email: email)
97
+
98
+ if users.empty?
99
+ exit_with_warning("The user '#{email}' could not be found.")
100
+ end
101
+
102
+ unless options[:force]
103
+ confirm_action!("Remove the user '#{email}'?", "'#{email}' left untouched.")
104
+ end
105
+
106
+ users.each(&:destroy)
107
+
108
+ prompt.ok("The user '#{email}' has been removed.")
109
+ end
110
+
111
+ desc "change_password", "Change a user's password"
112
+ option :password, type: :string, default: nil, aliases: :p
113
+ def change_password(email = nil)
114
+ unless email
115
+ users = PostDB::User.all
116
+
117
+ if users.empty?
118
+ exit_with_warning("There don't appear to be any users on this system.")
119
+ end
120
+
121
+ email = prompt.select("User:", users.map(&:email))
122
+ end
123
+
124
+ email = email.downcase
125
+
126
+ users = PostDB::User.where(email: email)
127
+
128
+ if users.empty?
129
+ exit_with_warning("The user '#{email}' could not be found.")
130
+ end
131
+
132
+ password = get_password_or_ask
133
+
134
+ users.each do |user|
135
+ user.update(password: password)
136
+ end
137
+
138
+ prompt.ok("The password for '#{email}' has been changed.")
139
+ end
140
+
141
+ map changepw: :change_password
142
+
143
+ default_task :list
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,35 @@
1
+ module PostDB
2
+ class Configuration
3
+ class << self
4
+ # Load the configuration from a file
5
+ #
6
+ # Arguments:
7
+ # path: (String) The path to the configuration file
8
+ #
9
+ # Example:
10
+ # >> PostDB::Configuration.load_file(path)
11
+ # => true
12
+ #
13
+ def load_file(path)
14
+ @configuration = YAML.load(File.read(path))
15
+
16
+ true
17
+ end
18
+
19
+ # Get a value from the configuration
20
+ #
21
+ # Arguments:
22
+ # name: (String)
23
+ #
24
+ # Example:
25
+ # >> PostDB::Configuration[:database]
26
+ # => { ... }
27
+ #
28
+ def [](name)
29
+ return nil unless @configuration
30
+
31
+ @configuration[name]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module PostDB
2
+ VERSION = '0.1.5'
3
+ end
@@ -0,0 +1,23 @@
1
+ module PostDB
2
+ class Database
3
+ class << self
4
+ # Setup the database
5
+ #
6
+ # Example:
7
+ # >> PostDB::Database.setup_with_configuration!
8
+ # => nil
9
+ #
10
+ def setup_with_configuration!
11
+ configuration = PostDB::Configuration[:database]
12
+
13
+ unless configuration.is_a?(Hash)
14
+ raise PostDB::SetupError.new(:missing_database_args)
15
+ end
16
+
17
+ ActiveRecord::Base.establish_connection(**configuration)
18
+
19
+ nil
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,197 @@
1
+ module PostDB
2
+ class DKIM
3
+ class << self
4
+ # The path to the directory where DKIM keys are stored
5
+ #
6
+ attr_reader :keys_directory
7
+
8
+ # The path to the trusted hosts file
9
+ #
10
+ attr_reader :trusted_hosts_path
11
+
12
+ # The path to the key table file
13
+ #
14
+ attr_reader :key_table_path
15
+
16
+ # The path to the signing table file
17
+ #
18
+ attr_reader :signing_table_path
19
+
20
+ # Setup the DKIM configuration
21
+ #
22
+ # Example:
23
+ # >> PostDB::DKIM.setup_with_configuration!
24
+ # => nil
25
+ #
26
+ def setup_with_configuration!
27
+ configuration = PostDB::Configuration[:dkim]
28
+
29
+ unless configuration.is_a?(Hash)
30
+ raise PostDB::SetupError.new(:missing_dkim_args)
31
+ end
32
+
33
+ unless configuration[:directory]
34
+ raise PostDB::SetupError.new(:missing_dkim_directory)
35
+ end
36
+
37
+ unless configuration[:trusted_hosts_path]
38
+ raise PostDB::SetupError.new(:missing_trusted_hosts_path)
39
+ end
40
+
41
+ unless configuration[:key_table_path]
42
+ raise PostDB::SetupError.new(:missing_key_table_path)
43
+ end
44
+
45
+ unless configuration[:signing_table_path]
46
+ raise PostDB::SetupError.new(:missing_signing_table_path)
47
+ end
48
+
49
+ @keys_directory = configuration[:directory]
50
+
51
+ unless File.directory?(@keys_directory)
52
+ FileUtils.mkdir_p(@keys_directory)
53
+ end
54
+
55
+ @trusted_hosts_path = configuration[:trusted_hosts_path]
56
+ @key_table_path = configuration[:key_table_path]
57
+ @signing_table_path = configuration[:signing_table_path]
58
+
59
+ true
60
+ end
61
+
62
+ # Generate the OpenDKIM configuration files
63
+ #
64
+ # Example:
65
+ # >> PostDB::DKIM.generate_configuration
66
+ # => nil
67
+ #
68
+ def generate_configuration
69
+ dump_private_keys
70
+ generate_trusted_hosts_configuration
71
+ generate_key_table_configuration
72
+ generate_signing_table_configuration
73
+ restart_opendkim
74
+
75
+ nil
76
+ end
77
+
78
+ private
79
+ # Get the passwd object for the opendkim user
80
+ #
81
+ # Example:
82
+ # >> PostDB::DKIM.passwd
83
+ # => #<struct Etc::Passwd name="opendkim", passwd="x", uid=101, gid=101, gecos="", dir="/var/run/opendkim", shell="/bin/false">
84
+ #
85
+ def passwd
86
+ Etc.passwd do |passwd|
87
+ next unless passwd.name == 'opendkim'
88
+
89
+ return passwd
90
+ end
91
+
92
+ nil
93
+ end
94
+
95
+ # Dump the private keys for each domain
96
+ #
97
+ # Example:
98
+ # >> PostDB::DKIM.dump_private_keys
99
+ # => nil
100
+ #
101
+ def dump_private_keys
102
+ domains = PostDB::Domain.all
103
+
104
+ dkim_keys = domains.map { |domain| File.basename(domain.dkim_path) }
105
+
106
+ keys_directory_glob = File.join(@keys_directory, '**', '*')
107
+
108
+ Dir.glob(keys_directory_glob).each do |key|
109
+ next if dkim_keys.include?(File.basename(key))
110
+
111
+ FileUtils.rm_f(key)
112
+ end
113
+
114
+ domains.each do |domain|
115
+ File.open(domain.dkim_path, 'w') do |file|
116
+ file.write(domain.dkim.to_pem)
117
+ end
118
+
119
+ next unless passwd
120
+
121
+ File.chown(passwd.uid, passwd.gid, domain.dkim_path)
122
+ File.chmod(0600, domain.dkim_path)
123
+ end
124
+
125
+ nil
126
+ end
127
+
128
+ # Generate the TrustedHosts configuration file
129
+ #
130
+ # Example:
131
+ # >> PostDB::DKIM.generate_trusted_hosts_configuration
132
+ # => nil
133
+ #
134
+ def generate_trusted_hosts_configuration
135
+ trusted_hosts = ['127.0.0.1', 'localhost']
136
+ trusted_hosts += PostDB::Domain.all.map { |domain| "*." + domain.name }
137
+
138
+ File.open(@trusted_hosts_path, 'w') do |file|
139
+ file.write(trusted_hosts.join("\n"))
140
+ end
141
+
142
+ nil
143
+ end
144
+
145
+ # Generate the KeyTable configuration file
146
+ #
147
+ # Example:
148
+ # >> PostDB::DKIM.generate_key_table_configuration
149
+ # => nil
150
+ #
151
+ def generate_key_table_configuration
152
+ selector = "mail"
153
+
154
+ key_table = Array.new
155
+ key_table += PostDB::Domain.all.map { |domain| selector + "._domainkey." + domain.name + " " + domain.name + ":" + selector + ":" + domain.dkim_path }
156
+
157
+ File.open(@key_table_path, 'w') do |file|
158
+ file.write(key_table.join("\n"))
159
+ end
160
+
161
+ nil
162
+ end
163
+
164
+ # Generate the SigningTable configuration file
165
+ #
166
+ # Example:
167
+ # >> PostDB::DKIM.generate_signing_table_configuration
168
+ # => nil
169
+ #
170
+ def generate_signing_table_configuration
171
+ selector = "mail"
172
+
173
+ signing_table = Array.new
174
+ signing_table += PostDB::Domain.all.map { |domain| "*@" + domain.name + " " + selector + "._domainkey." + domain.name }
175
+
176
+ File.open(@signing_table_path, 'w') do |file|
177
+ file.write(signing_table.join("\n"))
178
+ end
179
+
180
+ nil
181
+ end
182
+
183
+ # Restart the OpenDKIM daemon
184
+ #
185
+ # Example:
186
+ # >> PostDB::DKIM.restart_opendkim
187
+ # => true
188
+ #
189
+ def restart_opendkim
190
+ return false unless system("service opendkim stop > /dev/null 2>&1")
191
+ return false unless system("service opendkim start > /dev/null 2>&1")
192
+
193
+ true
194
+ end
195
+ end
196
+ end
197
+ end