postfix_admin 0.2.1 → 0.3.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +52 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +20 -0
  5. data/CHANGELOG.md +26 -12
  6. data/Gemfile +1 -1
  7. data/LICENSE +1 -1
  8. data/README.md +26 -20
  9. data/Rakefile +48 -1
  10. data/bin/console +6 -2
  11. data/db/reset.rb +7 -0
  12. data/db/seeds.rb +26 -0
  13. data/docker-admin/config.local.php +6 -1
  14. data/docker-app/Dockerfile +2 -6
  15. data/docker-app/my.cnf +2 -2
  16. data/docker-compose.yml +20 -20
  17. data/lib/postfix_admin/base.rb +119 -112
  18. data/lib/postfix_admin/cli.rb +267 -158
  19. data/lib/postfix_admin/doveadm.rb +32 -20
  20. data/lib/postfix_admin/{admin.rb → models/admin.rb} +25 -2
  21. data/lib/postfix_admin/{alias.rb → models/alias.rb} +16 -26
  22. data/lib/postfix_admin/{application_record.rb → models/application_record.rb} +1 -1
  23. data/lib/postfix_admin/{concerns → models/concerns}/existing_timestamp.rb +1 -2
  24. data/lib/postfix_admin/models/concerns/has_password.rb +16 -0
  25. data/lib/postfix_admin/models/domain.rb +112 -0
  26. data/lib/postfix_admin/models/domain_admin.rb +19 -0
  27. data/lib/postfix_admin/models/log.rb +20 -0
  28. data/lib/postfix_admin/models/mailbox.rb +126 -0
  29. data/lib/postfix_admin/models/quota2.rb +18 -0
  30. data/lib/postfix_admin/models.rb +8 -9
  31. data/lib/postfix_admin/runner.rb +91 -28
  32. data/lib/postfix_admin/version.rb +1 -1
  33. data/postfix_admin.gemspec +12 -10
  34. metadata +61 -34
  35. data/.github/workflows/ruby.yml +0 -37
  36. data/docker-app/docker-entrypoint.sh +0 -5
  37. data/docker-app-2.5/Dockerfile +0 -15
  38. data/lib/postfix_admin/concerns/dovecot_cram_md5_password.rb +0 -30
  39. data/lib/postfix_admin/domain.rb +0 -98
  40. data/lib/postfix_admin/domain_admin.rb +0 -8
  41. data/lib/postfix_admin/log.rb +0 -5
  42. data/lib/postfix_admin/mail_domain.rb +0 -9
  43. data/lib/postfix_admin/mailbox.rb +0 -89
  44. data/lib/postfix_admin/quota.rb +0 -6
  45. /data/lib/postfix_admin/{concerns → models/concerns}/.keep +0 -0
@@ -8,46 +8,41 @@ module PostfixAdmin
8
8
  attr_reader :config
9
9
 
10
10
  DEFAULT_CONFIG = {
11
- 'database' => 'mysql2://postfix:password@localhost/postfix',
12
- 'aliases' => 30,
13
- 'mailboxes' => 30,
14
- 'maxquota' => 100,
15
- 'scheme' => 'CRAM-MD5',
16
- 'passwordhash_prefix' => true
17
- }
11
+ "database" => "mysql2://postfix:password@localhost/postfix",
12
+ "aliases" => 30,
13
+ "mailboxes" => 30,
14
+ "maxquota" => 100,
15
+ "scheme" => "CRAM-MD5",
16
+ "passwordhash_prefix" => true
17
+ }.freeze
18
18
 
19
19
  def initialize(config)
20
20
  @config = {}
21
- @config[:database] = config['database']
22
- @config[:aliases] = config['aliases'] || 30
23
- @config[:mailboxes] = config['mailboxes'] || 30
24
- @config[:maxquota] = config['maxquota'] || 100
25
- @config[:scheme] = config['scheme'] || 'CRAM-MD5'
26
- @config[:passwordhash_prefix] = if config['passwordhash_prefix'].nil?
27
- true
21
+ @config[:database] = config["database"]
22
+ @config[:aliases] = config["aliases"] || DEFAULT_CONFIG["aliases"]
23
+ @config[:mailboxes] = config["mailboxes"] || DEFAULT_CONFIG["mailboxes"]
24
+ @config[:maxquota] = config["maxquota"] || DEFAULT_CONFIG["maxquota"]
25
+ @config[:scheme] = config["scheme"] || DEFAULT_CONFIG["scheme"]
26
+ @config[:passwordhash_prefix] = if config.has_key?("passwordhash_prefix")
27
+ config["passwordhash_prefix"]
28
28
  else
29
- config['passwordhash_prefix']
29
+ DEFAULT_CONFIG["passwordhash_prefix"]
30
30
  end
31
31
  end
32
32
 
33
33
  def db_setup
34
- raise "'database' parameter is required in '#{CLI.config_file}'" unless @config[:database]
34
+ database = ENV.fetch("DATABASE_URL") { @config[:database] }
35
35
 
36
- uri = URI.parse(@config[:database])
37
-
38
- if uri.scheme == "mysql"
39
- uri.scheme = "mysql2"
40
- warn("Deprecation Warning: Use 'mysql2' as a DB adopter instead of 'mysql' in '#{CLI.config_file}'")
36
+ unless database
37
+ raise_error "'database' parameter is required in '#{CLI.config_file}' or specify 'DATABASE_URL' environment variable"
41
38
  end
42
39
 
43
- if uri.scheme != "mysql2"
44
- raise "'#{uri.scheme}' is not supported as a DB adopter. Use 'mysql2' instead in '#{CLI.config_file}'."
45
- end
40
+ uri = URI.parse(database)
46
41
 
47
42
  ActiveRecord::Base.establish_connection(uri.to_s)
48
43
 
49
44
  rescue LoadError => e
50
- raise e.message
45
+ raise_error e.message
51
46
  end
52
47
 
53
48
  def add_admin_domain(user_name, domain_name)
@@ -57,11 +52,11 @@ module PostfixAdmin
57
52
  domain = Domain.find(domain_name)
58
53
 
59
54
  if admin.has_domain?(domain)
60
- raise Error, "#{user_name} is already registered as admin of #{domain_name}."
55
+ raise_error "Admin '#{user_name}' has already been registered for Domain '#{domain_name}'"
61
56
  end
62
57
 
63
58
  admin.rel_domains << domain
64
- admin.save or raise "Relation Error: Domain of Admin"
59
+ admin.save || raise_error("Relation Error: Domain of Admin")
65
60
  end
66
61
 
67
62
  def delete_admin_domain(user_name, domain_name)
@@ -71,181 +66,193 @@ module PostfixAdmin
71
66
  domain_admin_query = admin.domain_admins.where(domain: domain_name)
72
67
 
73
68
  unless domain_admin_query.take
74
- raise Error, "#{user_name} is not registered as admin of #{domain_name}."
69
+ raise_error "#{user_name} is not registered as admin of #{domain_name}."
75
70
  end
76
71
 
77
72
  domain_admin_query.delete_all
78
73
  end
79
74
 
80
75
  def add_admin(username, password)
81
- password_check(password)
76
+ validate_password(password)
82
77
 
83
78
  if Admin.exists?(username)
84
- raise Error, "#{username} is already registered as admin."
79
+ raise_error "Admin has already been registered: #{username}"
85
80
  end
81
+
86
82
  admin = Admin.new
87
83
  admin.attributes = {
88
84
  username: username,
89
- password: password,
85
+ password: password
90
86
  }
91
- unless admin.save
92
- raise "Could not save Admin #{admin.errors.map(&:to_s).join}"
93
- end
94
- end
95
-
96
- def add_account(address, password, in_name = nil)
97
- name = in_name || ''
98
- password_check(password)
99
-
100
- if address !~ /.+\@.+\..+/
101
- raise Error, "Invalid mail address #{address}"
102
- end
103
- user, domain_name = address_split(address)
104
- path = "#{domain_name}/#{address}/"
105
87
 
106
- unless Domain.exists?(domain_name)
107
- raise Error, "Could not find domain #{domain_name}"
108
- end
88
+ raise_save_error(admin) unless admin.save
89
+ end
109
90
 
110
- if Alias.exists?(address)
111
- raise Error, "#{address} is already registered."
112
- end
91
+ # Adds an email account that consists of a Mailbox and an Alias.
92
+ def add_account(address, password, name: "")
93
+ validate_account(address, password)
113
94
 
114
- domain = Domain.find(domain_name)
95
+ local_part, domain_name = address_split(address)
96
+ domain_must_exist!(domain_name)
115
97
 
116
98
  attributes = {
117
- username: address,
118
- password: password,
119
- name: name,
120
- maildir: path,
121
- local_part: user,
122
- quota_mb: @config[:maxquota]
99
+ local_part: local_part,
100
+ domain: domain_name,
101
+ password: password,
102
+ name: name,
103
+ quota: @config[:maxquota] * KB_TO_MB
123
104
  }
124
105
 
106
+ # An Alias also will be added when a Mailbox is saved.
125
107
  mailbox = Mailbox.new(attributes)
126
108
 
127
- domain.rel_mailboxes << mailbox
128
-
129
- unless domain.save
130
- raise "Could not save Mailbox and Domain #{mailbox.errors.map(&:to_s).join} #{domain.errors.map(&:to_s).join}"
131
- end
109
+ raise_save_error(mailbox) unless mailbox.save
132
110
  end
133
111
 
134
112
  def add_alias(address, goto)
135
113
  if Mailbox.exists?(address)
136
- raise Error, "mailbox #{address} is already registered!"
137
- end
138
- if Alias.exists?(address)
139
- raise Error, "alias #{address} is already registered!"
114
+ raise_error "Mailbox has already been registered: #{address}"
140
115
  end
141
116
 
142
- local_part, domain_name = address_split(address)
117
+ alias_must_not_exist!(address)
143
118
 
144
- unless Domain.exists?(domain_name)
145
- raise Error, "Invalid domain! #{domain_name}"
146
- end
119
+ local_part, domain_name = address_split(address)
147
120
 
148
- domain = Domain.find(domain_name)
121
+ domain = find_domain(domain_name)
149
122
 
150
123
  attributes = {
151
- local_part: local_part,
124
+ address: address,
152
125
  goto: goto
153
126
  }
127
+
154
128
  domain.rel_aliases << Alias.new(attributes)
155
- domain.save or raise "Could not save Alias"
129
+
130
+ raise_save_error(domain) unless domain.save
156
131
  end
157
132
 
158
133
  def delete_alias(address)
159
134
  if Mailbox.exists?(address)
160
- raise Error, "Can not delete mailbox by delete_alias. Use delete_account"
135
+ raise_error "Can not delete mailbox by delete_alias. Use delete_account"
161
136
  end
162
137
 
163
138
  unless Alias.exists?(address)
164
- raise Error, "#{address} is not found!"
139
+ raise_error "#{address} is not found!"
165
140
  end
166
141
 
167
142
  Alias.where(address: address).delete_all
168
143
  end
169
144
 
170
- def add_domain(domain_name)
145
+ def add_domain(domain_name, description: nil)
171
146
  domain_name = domain_name.downcase
172
- if domain_name !~ /.+\..+/
173
- raise Error, "Ivalid domain! #{domain_name}"
147
+
148
+ unless valid_domain_name?(domain_name)
149
+ raise_error "Invalid domain name: #{domain_name}"
174
150
  end
151
+
175
152
  if Domain.exists?(domain_name)
176
- raise Error, "#{domain_name} is already registered!"
153
+ raise_error "Domain has already been registered: #{domain_name}"
177
154
  end
155
+
156
+ new_description = description || domain_name
157
+
178
158
  domain = Domain.new
179
159
  domain.attributes = {
180
160
  domain: domain_name,
181
- description: domain_name,
161
+ description: new_description,
182
162
  aliases: @config[:aliases],
183
163
  mailboxes: @config[:mailboxes],
184
- maxquota: @config[:maxquota],
164
+ maxquota: @config[:maxquota]
185
165
  }
186
166
  domain.save!
187
167
  end
188
168
 
189
169
  def delete_domain(domain_name)
190
170
  domain_name = domain_name.downcase
191
- unless Domain.exists?(domain_name)
192
- raise Error, "Could not find domain #{domain_name}"
193
- end
194
171
 
195
- domain = Domain.find(domain_name)
196
- domain.rel_mailboxes.delete_all
197
- domain.rel_aliases.delete_all
172
+ domain = find_domain(domain_name)
198
173
 
199
174
  admin_names = domain.admins.map(&:username)
200
175
 
201
- domain.admins.delete_all
202
-
203
- admin_names.each do |name|
204
- next unless Admin.exists?(name)
205
-
206
- admin = Admin.find(name)
207
-
208
- # check if the admin is needed or not
209
- if admin.rel_domains.empty?
210
- admin.destroy
211
- end
212
- end
213
-
214
- domain.destroy
176
+ domain.destroy!
215
177
  end
216
178
 
217
179
  def delete_admin(user_name)
218
180
  unless Admin.exists?(user_name)
219
- raise Error, "Could not find admin #{user_name}"
181
+ raise_error "Could not find admin #{user_name}"
220
182
  end
221
183
 
222
184
  admin = Admin.find(user_name)
223
- admin.rel_domains.delete_all
224
185
  admin.destroy!
225
186
  end
226
187
 
227
188
  def delete_account(address)
228
189
  unless Alias.exists?(address) && Mailbox.exists?(address)
229
- raise Error, "Could not find account #{address}"
190
+ raise_error "Could not find account: #{address}"
230
191
  end
231
192
 
232
- Mailbox.where(username: address).delete_all
233
- Alias.where(address: address).delete_all
193
+ mailbox = Mailbox.find(address)
194
+ mailbox.destroy!
195
+ end
196
+
197
+ private
198
+
199
+ def find_domain(domain_name)
200
+ domain_must_exist!(domain_name)
201
+ Domain.find(domain_name)
202
+ end
203
+
204
+ def raise_error(message)
205
+ raise PostfixAdmin::Error, message
206
+ end
207
+
208
+ def raise_save_error(obj)
209
+ raise_error "Failed to save #{obj.class}: #{obj.errors.full_messages.join(', ')}"
234
210
  end
235
211
 
236
212
  def address_split(address)
237
213
  address.split('@')
238
214
  end
239
215
 
240
- private
216
+ def valid_domain_name?(domain_name)
217
+ /.+\..+/.match?(domain_name)
218
+ end
219
+
220
+ def valid_email_address?(address)
221
+ /.+@.+\..+/.match?(address)
222
+ end
223
+
224
+ def domain_must_exist!(domain_name)
225
+ unless Domain.exists?(domain_name)
226
+ raise_error "Could not find domain: #{domain_name}"
227
+ end
228
+ end
229
+
230
+ def alias_must_not_exist!(address)
231
+ if Alias.exists?(address)
232
+ raise_error "Alias has already been registered: #{address}"
233
+ end
234
+ end
241
235
 
242
236
  def admin_domain_check(user_name, domain_name)
243
- raise Error, "#{user_name} is not registered as admin." unless Admin.exists?(user_name)
244
- raise Error, "Could not find domain #{domain_name}" unless Domain.exists?(domain_name)
237
+ unless Admin.exists?(user_name)
238
+ raise_error "#{user_name} is not registered as admin."
239
+ end
240
+
241
+ domain_must_exist!(domain_name)
245
242
  end
246
243
 
247
- def password_check(password)
248
- raise Error, "Empty password" if password.nil? || password.empty?
244
+ def validate_password(password)
245
+ raise_error "Empty password" if password.nil? || password.empty?
246
+ end
247
+
248
+ def validate_account(address, password)
249
+ validate_password(password)
250
+
251
+ unless valid_email_address?(address)
252
+ raise_error "Invalid email address: #{address}"
253
+ end
254
+
255
+ alias_must_not_exist!(address)
249
256
  end
250
257
  end
251
258
  end