postfix_admin 0.1.1 → 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.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +37 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +22 -0
- data/README.md +47 -32
- data/Rakefile +6 -0
- data/bin/console +18 -0
- data/docker-admin/Dockerfile +6 -0
- data/docker-admin/config.local.php +21 -0
- data/docker-app-2.5/Dockerfile +15 -0
- data/docker-app/Dockerfile +25 -0
- data/docker-app/docker-entrypoint.sh +5 -0
- data/docker-app/my.cnf +5 -0
- data/docker-compose.yml +46 -0
- data/docker-db/postfix.v1841.sql +383 -0
- data/{spec/postfix_test.sql → docker-db/postfix.v352.sql} +1 -28
- data/docker-db/postfix.v740.sql +269 -0
- data/{bin → exe}/postfix_admin +1 -0
- data/lib/postfix_admin.rb +1 -1
- data/lib/postfix_admin/admin.rb +62 -0
- data/lib/postfix_admin/alias.rb +65 -0
- data/lib/postfix_admin/application_record.rb +44 -0
- data/lib/postfix_admin/base.rb +120 -75
- data/lib/postfix_admin/cli.rb +173 -58
- data/lib/postfix_admin/concerns/.keep +0 -0
- data/lib/postfix_admin/concerns/dovecot_cram_md5_password.rb +30 -0
- data/lib/postfix_admin/concerns/existing_timestamp.rb +18 -0
- data/lib/postfix_admin/domain.rb +98 -0
- data/lib/postfix_admin/domain_admin.rb +8 -0
- data/lib/postfix_admin/doveadm.rb +37 -0
- data/lib/postfix_admin/log.rb +5 -0
- data/lib/postfix_admin/mail_domain.rb +9 -0
- data/lib/postfix_admin/mailbox.rb +89 -0
- data/lib/postfix_admin/models.rb +10 -170
- data/lib/postfix_admin/quota.rb +6 -0
- data/lib/postfix_admin/runner.rb +108 -36
- data/lib/postfix_admin/version.rb +1 -1
- data/postfix_admin.gemspec +22 -12
- metadata +80 -55
- data/spec/base_spec.rb +0 -235
- data/spec/cli_spec.rb +0 -286
- data/spec/models_spec.rb +0 -146
- data/spec/postfix_admin.conf +0 -5
- data/spec/runner_spec.rb +0 -194
- 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,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,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
|
data/lib/postfix_admin/models.rb
CHANGED
@@ -1,170 +1,10 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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'
|