postfix_admin 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +37 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +2 -0
  5. data/CHANGELOG.md +22 -0
  6. data/README.md +47 -32
  7. data/Rakefile +6 -0
  8. data/bin/console +18 -0
  9. data/docker-admin/Dockerfile +6 -0
  10. data/docker-admin/config.local.php +21 -0
  11. data/docker-app-2.5/Dockerfile +15 -0
  12. data/docker-app/Dockerfile +25 -0
  13. data/docker-app/docker-entrypoint.sh +5 -0
  14. data/docker-app/my.cnf +5 -0
  15. data/docker-compose.yml +46 -0
  16. data/docker-db/postfix.v1841.sql +383 -0
  17. data/{spec/postfix_test.sql → docker-db/postfix.v352.sql} +1 -28
  18. data/docker-db/postfix.v740.sql +269 -0
  19. data/{bin → exe}/postfix_admin +1 -0
  20. data/lib/postfix_admin.rb +1 -1
  21. data/lib/postfix_admin/admin.rb +62 -0
  22. data/lib/postfix_admin/alias.rb +65 -0
  23. data/lib/postfix_admin/application_record.rb +44 -0
  24. data/lib/postfix_admin/base.rb +120 -75
  25. data/lib/postfix_admin/cli.rb +173 -58
  26. data/lib/postfix_admin/concerns/.keep +0 -0
  27. data/lib/postfix_admin/concerns/dovecot_cram_md5_password.rb +30 -0
  28. data/lib/postfix_admin/concerns/existing_timestamp.rb +18 -0
  29. data/lib/postfix_admin/domain.rb +98 -0
  30. data/lib/postfix_admin/domain_admin.rb +8 -0
  31. data/lib/postfix_admin/doveadm.rb +37 -0
  32. data/lib/postfix_admin/log.rb +5 -0
  33. data/lib/postfix_admin/mail_domain.rb +9 -0
  34. data/lib/postfix_admin/mailbox.rb +89 -0
  35. data/lib/postfix_admin/models.rb +10 -170
  36. data/lib/postfix_admin/quota.rb +6 -0
  37. data/lib/postfix_admin/runner.rb +108 -36
  38. data/lib/postfix_admin/version.rb +1 -1
  39. data/postfix_admin.gemspec +22 -12
  40. metadata +80 -55
  41. data/spec/base_spec.rb +0 -235
  42. data/spec/cli_spec.rb +0 -286
  43. data/spec/models_spec.rb +0 -146
  44. data/spec/postfix_admin.conf +0 -5
  45. data/spec/runner_spec.rb +0 -194
  46. data/spec/spec_helper.rb +0 -159
File without changes
@@ -0,0 +1,30 @@
1
+ require 'active_support/concern'
2
+
3
+ module DovecotCramMD5Password
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ validates :password_unencrypted, length: { minimum: 5 }, allow_blank: true
8
+ validates_confirmation_of :password_unencrypted, allow_blank: true
9
+
10
+ validate do |record|
11
+ record.errors.add(:password_unencrypted, :blank) unless record.password.present?
12
+ end
13
+
14
+ attr_reader :password_unencrypted
15
+ attr_accessor :password_unencrypted_confirmation
16
+ end
17
+
18
+ def password_unencrypted=(unencrypted_password)
19
+ if unencrypted_password.nil?
20
+ self.password = nil
21
+ elsif !unencrypted_password.empty?
22
+ @password_unencrypted = unencrypted_password
23
+ self.password = DovecotCrammd5.calc(unencrypted_password)
24
+ end
25
+ end
26
+
27
+ def authenticate(unencrypted_password)
28
+ password == DovecotCrammd5.calc(unencrypted_password) && self
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_support/concern'
2
+
3
+ module ExistingTimestamp
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ private
8
+
9
+ def timestamp_attributes_for_create
10
+ ["created"]
11
+ end
12
+
13
+ def timestamp_attributes_for_update
14
+ ["modified"]
15
+ end
16
+ end
17
+
18
+ end
@@ -0,0 +1,98 @@
1
+ module PostfixAdmin
2
+ class Domain < ApplicationRecord
3
+ self.table_name = :domain
4
+ self.primary_key = :domain
5
+
6
+ validates :domain, presence: true, uniqueness: { case_sensitive: false },
7
+ format: { with: RE_DOMAIN_NAME_LIKE_WITH_ANCHORS,
8
+ message: "must be a valid domain name" }
9
+ validates :transport, presence: true
10
+
11
+ validates :aliases, presence: true,
12
+ numericality: { only_integer: true,
13
+ greater_than_or_equal_to: 0 }
14
+ validates :mailboxes, presence: true,
15
+ numericality: { only_integer: true,
16
+ greater_than_or_equal_to: 0 }
17
+ validates :maxquota, presence: true,
18
+ numericality: { only_integer: true,
19
+ greater_than_or_equal_to: 0 }
20
+
21
+ has_many :rel_mailboxes, class_name: "Mailbox", foreign_key: :domain,
22
+ dependent: :destroy
23
+ has_many :rel_aliases, class_name: "Alias", foreign_key: :domain,
24
+ dependent: :destroy
25
+
26
+ has_many :domain_admins, foreign_key: :domain, dependent: :delete_all
27
+ has_many :admins, through: :domain_admins
28
+
29
+ before_validation do |domain|
30
+ domain.domain = domain.domain.downcase unless domain.domain.empty?
31
+ domain.transport = "virtual"
32
+ end
33
+
34
+ scope :without_all, -> { where.not(domain: "ALL") }
35
+
36
+ def pure_aliases
37
+ rel_aliases.pure
38
+ end
39
+
40
+ def aliases_unlimited?
41
+ aliases.zero?
42
+ end
43
+
44
+ def mailboxes_unlimited?
45
+ mailboxes.zero?
46
+ end
47
+
48
+ def aliases_str
49
+ num_str(aliases)
50
+ end
51
+
52
+ def mailboxes_str
53
+ num_str(mailboxes)
54
+ end
55
+
56
+ def aliases_short_str
57
+ num_short_str(aliases)
58
+ end
59
+
60
+ def mailboxes_short_str
61
+ num_short_str(mailboxes)
62
+ end
63
+
64
+ def maxquota_str
65
+ if maxquota.zero?
66
+ "Unlimited"
67
+ else
68
+ "#{maxquota} MB"
69
+ end
70
+ end
71
+
72
+ def maxquota_short_str
73
+ if maxquota.zero?
74
+ "--"
75
+ else
76
+ "#{maxquota} MB"
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def num_str(num)
83
+ if num.zero?
84
+ "Unlimited"
85
+ else
86
+ num.to_s
87
+ end
88
+ end
89
+
90
+ def num_short_str(num)
91
+ if num.zero?
92
+ "--"
93
+ else
94
+ num.to_s
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,8 @@
1
+ module PostfixAdmin
2
+ class DomainAdmin < ApplicationRecord
3
+ self.table_name = :domain_admins
4
+
5
+ belongs_to :admin, primary_key: :username, foreign_key: :username
6
+ belongs_to :rel_domain, class_name: "Domain", foreign_key: :domain
7
+ end
8
+ end
@@ -0,0 +1,37 @@
1
+
2
+ require 'open3'
3
+ require 'shellwords'
4
+
5
+ module PostfixAdmin
6
+ class Doveadm
7
+ def self.schemes
8
+ result = `#{self.command_name} -l`
9
+ result.split
10
+ end
11
+
12
+ def self.password(in_password, in_scheme, prefix)
13
+ password = Shellwords.escape(in_password)
14
+ scheme = Shellwords.escape(in_scheme)
15
+ stdin, stdout, stderr = Open3.popen3("#{self.command_name} -s #{scheme} -p #{password}")
16
+ if stderr.readlines.to_s =~ /Fatal:/
17
+ raise Error, stderr.readlines
18
+ else
19
+ res = stdout.readlines.first.chomp
20
+ if prefix
21
+ res
22
+ else
23
+ res.gsub("{#{scheme}}", "")
24
+ end
25
+ end
26
+ end
27
+
28
+ def self.command_name
29
+ begin
30
+ Open3.capture3("doveadm pw -l")[2].exited?
31
+ "doveadm pw"
32
+ rescue Errno::ENOENT
33
+ "dovecotpw"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,5 @@
1
+ module PostfixAdmin
2
+ class Log < ApplicationRecord
3
+ self.table_name = :log
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ module PostfixAdmin
2
+ class MailDomain < ApplicationRecord
3
+ self.table_name = :domain
4
+ self.primary_key = :domain
5
+
6
+ has_many :addresses, class_name: "Mailbox", foreign_key: :domain,
7
+ dependent: :destroy
8
+ end
9
+ end
@@ -0,0 +1,89 @@
1
+ module PostfixAdmin
2
+ class Mailbox < ApplicationRecord
3
+ self.table_name = :mailbox
4
+ self.primary_key = :username
5
+
6
+ include DovecotCramMD5Password
7
+
8
+ attribute :quota_mb, :integer
9
+
10
+ validates :username, presence: true, uniqueness: { case_sensitive: false },
11
+ format: { with: RE_EMAIL_LIKE_WITH_ANCHORS,
12
+ message: "must be a valid email address" }
13
+ validates :maildir, presence: true, uniqueness: { case_sensitive: false }
14
+ validates :local_part, presence: true
15
+ validates :quota, presence: true,
16
+ numericality: { only_integer: true,
17
+ greater_than_or_equal_to: 0 }
18
+ validates :quota_mb, presence: true,
19
+ numericality: { only_integer: true,
20
+ greater_than_or_equal_to: 0 }
21
+
22
+ belongs_to :rel_domain, class_name: "Domain", foreign_key: :domain
23
+ has_one :alias, foreign_key: :address, dependent: :destroy
24
+ has_one :quota_usage, class_name: "Quota", foreign_key: :username,
25
+ dependent: :destroy
26
+
27
+ validate on: :create do |mailbox|
28
+ domain = mailbox.rel_domain
29
+ if !domain.mailboxes.zero? && domain.rel_mailboxes.count >= domain.mailboxes
30
+ message = "already has the maximum number of mailboxes " \
31
+ "(maximum is #{domain.mailboxes} mailboxes)"
32
+ mailbox.errors.add(:domain, message)
33
+ end
34
+ end
35
+
36
+ # just in case
37
+ validate on: :update do |mailbox|
38
+ mailbox.errors.add(:username, 'cannot be changed') if mailbox.username_changed?
39
+ mailbox.errors.add(:local_part, 'cannot be changed') if mailbox.local_part_changed?
40
+ end
41
+
42
+ validate do |mailbox|
43
+ domain = mailbox.rel_domain
44
+
45
+ unless domain.maxquota.zero?
46
+ if mailbox.quota_mb.zero?
47
+ mailbox.errors.add(:quota_mb, "cannot be 0")
48
+ elsif mailbox.quota_mb > domain.maxquota
49
+ message = "must be less than or equal to #{domain.maxquota} (MB)"
50
+ mailbox.errors.add(:quota_mb, message)
51
+ end
52
+ end
53
+ end
54
+
55
+ before_validation do |mailbox|
56
+ mailbox.name = "" if mailbox.name.nil?
57
+ if mailbox.quota_mb
58
+ mailbox.quota = mailbox.quota_mb * KB_TO_MB
59
+ elsif mailbox.quota
60
+ mailbox.quota_mb = mailbox.quota / KB_TO_MB
61
+ else
62
+ mailbox.quota_mb = 0
63
+ mailbox.quota = 0
64
+ end
65
+ mailbox.username = "#{mailbox.local_part}@#{mailbox.domain}"
66
+ mailbox.maildir = "#{mailbox.domain}/#{mailbox.username}/"
67
+ mailbox.build_alias(local_part: mailbox.local_part, goto: mailbox.username,
68
+ domain: mailbox.domain)
69
+ end
70
+
71
+ def quota_usage_str
72
+ if quota_usage
73
+ usage_mb = quota_usage.bytes / KB_TO_MB
74
+ usage_mb.to_s
75
+ else
76
+ "0"
77
+ end
78
+ end
79
+
80
+ def quota_str
81
+ if quota.zero?
82
+ "--"
83
+ else
84
+ quota_mb = quota / KB_TO_MB
85
+ "#{quota_mb} MB"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,170 +1,10 @@
1
- require 'data_mapper'
2
-
3
- module PostfixAdmin
4
- class Admin
5
- include ::DataMapper::Resource
6
- property :username, String, :key => true
7
- property :password, String, :length => 0..255
8
- property :created, DateTime, :default => DateTime.now
9
- property :modified, DateTime, :default => DateTime.now
10
-
11
- has n, :domain_admins, :child_key => :username
12
- has n, :domains, :model => 'Domain', :through => :domain_admins, :via => :domain
13
- storage_names[:default] = 'admin'
14
-
15
- def has_domain?(domain_name)
16
- if super_admin?
17
- Domain.exist?(domain_name)
18
- else
19
- exist_domain?(domain_name)
20
- end
21
- end
22
-
23
- def super_admin=(value)
24
- if value
25
- domains << Domain.find('ALL')
26
- save or raise "Could not save ALL domain for Admin"
27
- else
28
- domain_admins(:domain_name => 'ALL').destroy or raise "Could not destroy DoaminAdmin for Admin"
29
- end
30
- end
31
-
32
- def super_admin?
33
- exist_domain?('ALL')
34
- end
35
-
36
- def clear_domains
37
- domains.clear
38
- save or raise "Could save Admin"
39
- end
40
-
41
- def self.find(username)
42
- Admin.first(:username => username)
43
- end
44
-
45
- def self.exist?(username)
46
- !!Admin.find(username)
47
- end
48
-
49
- private
50
-
51
- def exist_domain?(domain_name)
52
- !!domains.first(:domain_name => domain_name)
53
- end
54
- end
55
-
56
- class Domain
57
- include ::DataMapper::Resource
58
- property :domain_name, String, :field => 'domain', :key => true
59
- property :maxaliases, Integer, :field => 'aliases'
60
- property :maxmailboxes, Integer, :field => 'mailboxes'
61
- property :maxquota, Integer
62
- property :transport, String, :default => 'virtual'
63
- property :backupmx, Integer, :default => 0
64
- property :description, String
65
- property :created, DateTime, :default => DateTime.now
66
- property :modified, DateTime, :default => DateTime.now
67
-
68
- has n, :domain_admins, :child_key => :domain_name
69
- has n, :admins, :model => 'Admin', :through => :domain_admins
70
-
71
- has n, :mailboxes, :model => 'Mailbox', :child_key => :domain_name
72
- has n, :aliases, :model => 'Alias', :child_key => :domain_name
73
- storage_names[:default] = 'domain'
74
-
75
- def self.all_without_special_domain
76
- Domain.all(:domain_name.not => 'ALL')
77
- end
78
-
79
- def self.find(domain)
80
- Domain.first(:domain_name => domain)
81
- end
82
-
83
- def self.exist?(domain)
84
- !!Domain.find(domain)
85
- end
86
-
87
- def self.num_total_aliases
88
- Alias.count - Mailbox.count
89
- end
90
-
91
- def num_total_aliases
92
- aliases.count - mailboxes.count
93
- end
94
-
95
- def clear_admins
96
- admins.clear
97
- save or raise "Could not save Domain"
98
- end
99
- end
100
-
101
- class DomainAdmin
102
- include ::DataMapper::Resource
103
- property :created, DateTime, :default => DateTime.now
104
- property :domain_name, String, :field => 'domain', :key => true
105
- property :username, String, :key => true
106
-
107
- belongs_to :domain, :model => 'Domain', :child_key => :domain_name
108
- belongs_to :admin, :model => 'Admin', :child_key => :username
109
- storage_names[:default] = 'domain_admins'
110
- end
111
-
112
- class Mailbox
113
- include ::DataMapper::Resource
114
- property :username, String, :key => true
115
- property :name, String
116
- property :domain_name, String, :field => 'domain'
117
- property :password, String, :length => 0..255
118
- property :maildir, String
119
- property :quota, Integer
120
- # property :local_part, String
121
- property :created, DateTime, :default => DateTime.now
122
- property :modified, DateTime, :default => DateTime.now
123
-
124
- belongs_to :domain, :model => 'Domain', :child_key => :domain_name
125
-
126
- storage_names[:default] = 'mailbox'
127
-
128
- def self.find(username)
129
- Mailbox.first(:username => username)
130
- end
131
-
132
- def self.exist?(username)
133
- !!Mailbox.find(username)
134
- end
135
- end
136
-
137
- class Alias
138
- include ::DataMapper::Resource
139
- property :address, String, :key => true
140
- property :goto, Text
141
- property :domain_name, String, :field => 'domain'
142
- property :created, DateTime, :default => DateTime.now
143
- property :modified, DateTime, :default => DateTime.now
144
-
145
- belongs_to :domain, :model => 'Domain', :child_key => :domain_name
146
-
147
- storage_names[:default] = 'alias'
148
-
149
- def self.mailbox(address)
150
- mail_alias = Alias.new
151
- mail_alias.attributes = {
152
- :address => address,
153
- :goto => address,
154
- }
155
- mail_alias
156
- end
157
-
158
- def self.find(address)
159
- Alias.first(:address => address)
160
- end
161
-
162
- def self.exist?(address)
163
- !!Alias.find(address)
164
- end
165
-
166
- def mailbox?
167
- Mailbox.exist?(address)
168
- end
169
- end
170
- end
1
+ require 'active_record'
2
+ require 'postfix_admin/application_record'
3
+ require 'postfix_admin/admin'
4
+ require 'postfix_admin/domain'
5
+ require 'postfix_admin/mailbox'
6
+ require 'postfix_admin/alias'
7
+ require 'postfix_admin/domain_admin'
8
+ require 'postfix_admin/log'
9
+ require 'postfix_admin/mail_domain'
10
+ require 'postfix_admin/quota'