postfix_admin 0.1.0 → 0.2.0
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 +7 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +18 -0
- data/Dockerfile +24 -0
- data/README.md +57 -27
- data/Rakefile +6 -0
- data/bin/console +18 -0
- data/docker-compose.yml +24 -0
- data/docker-entrypoint.sh +5 -0
- data/exe/postfix_admin +6 -0
- data/lib/postfix_admin.rb +4 -0
- data/lib/postfix_admin/admin.rb +52 -0
- data/lib/postfix_admin/alias.rb +65 -0
- data/lib/postfix_admin/application_record.rb +44 -0
- data/lib/postfix_admin/base.rb +138 -81
- data/lib/postfix_admin/cli.rb +321 -119
- 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 +32 -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 -171
- data/lib/postfix_admin/quota.rb +6 -0
- data/lib/postfix_admin/runner.rb +136 -30
- data/lib/postfix_admin/version.rb +1 -1
- data/postfix_admin.gemspec +20 -11
- metadata +91 -54
- data/bin/postfix_admin +0 -9
- data/spec/base_spec.rb +0 -218
- data/spec/cli_spec.rb +0 -165
- data/spec/models_spec.rb +0 -136
- data/spec/postfix_admin.conf +0 -5
- data/spec/postfix_test.sql +0 -250
- data/spec/runner_spec.rb +0 -144
- data/spec/spec_helper.rb +0 -160
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,32 @@
|
|
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)
|
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
|
+
stdout.readlines.first.chomp
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.command_name
|
24
|
+
begin
|
25
|
+
Open3.capture3("doveadm pw -l")[2].exited?
|
26
|
+
"doveadm pw"
|
27
|
+
rescue Errno::ENOENT
|
28
|
+
"dovecotpw"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
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,171 +1,10 @@
|
|
1
|
-
require '
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def new(year = -4712, mon = 1, mday = 1, hour = 0, min = 0, sec = 0, offset = 0, start = Date::ITALY)
|
12
|
-
if year == 0
|
13
|
-
nil
|
14
|
-
else
|
15
|
-
org_new(year, mon, mday, hour, min, sec, offset, start)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
module PostfixAdmin
|
22
|
-
class Admin
|
23
|
-
include ::DataMapper::Resource
|
24
|
-
property :username, String, :key => true
|
25
|
-
property :password, String
|
26
|
-
property :created, DateTime, :default => DateTime.now
|
27
|
-
property :modified, DateTime, :default => DateTime.now
|
28
|
-
|
29
|
-
has n, :domain_admins, :child_key => :username
|
30
|
-
has n, :domains, :model => 'Domain', :through => :domain_admins, :via => :domain
|
31
|
-
storage_names[:default] = 'admin'
|
32
|
-
|
33
|
-
def has_domain?(domain_name)
|
34
|
-
if super_admin?
|
35
|
-
Domain.exist?(domain_name)
|
36
|
-
else
|
37
|
-
exist_domain?(domain_name)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def super_admin=(value)
|
42
|
-
if value
|
43
|
-
domains << Domain.find('ALL')
|
44
|
-
save or raise "Could not save ALL domain for Admin"
|
45
|
-
else
|
46
|
-
domain_admins(:domain_name => 'ALL').destroy or raise "Could not destroy DoaminAdmin for Admin"
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
def super_admin?
|
51
|
-
exist_domain?('ALL')
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.find(username)
|
55
|
-
Admin.first(:username => username)
|
56
|
-
end
|
57
|
-
|
58
|
-
def self.exist?(username)
|
59
|
-
!!Admin.find(username)
|
60
|
-
end
|
61
|
-
|
62
|
-
def self.unnecessary
|
63
|
-
all.delete_if do |admin|
|
64
|
-
admin.domains.size > 0
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
private
|
69
|
-
|
70
|
-
def exist_domain?(domain_name)
|
71
|
-
!!domains.first(:domain_name => domain_name)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
class Domain
|
76
|
-
include ::DataMapper::Resource
|
77
|
-
property :domain_name, String, :field => 'domain', :key => true
|
78
|
-
property :maxaliases, Integer, :field => 'aliases'
|
79
|
-
property :maxmailboxes, Integer, :field => 'mailboxes'
|
80
|
-
property :maxquota, Integer
|
81
|
-
property :transport, String, :default => 'virtual'
|
82
|
-
property :backupmx, Integer, :default => 0
|
83
|
-
property :description, String
|
84
|
-
property :created, DateTime, :default => DateTime.now
|
85
|
-
property :modified, DateTime, :default => DateTime.now
|
86
|
-
|
87
|
-
has n, :domain_admins, :child_key => :domain_name
|
88
|
-
has n, :admins, :model => 'Admin', :through => :domain_admins
|
89
|
-
|
90
|
-
has n, :mailboxes, :model => 'Mailbox', :child_key => :domain_name
|
91
|
-
has n, :aliases, :model => 'Alias', :child_key => :domain_name
|
92
|
-
storage_names[:default] = 'domain'
|
93
|
-
|
94
|
-
def self.all_without_special_domain
|
95
|
-
Domain.all(:domain_name.not => 'ALL')
|
96
|
-
end
|
97
|
-
|
98
|
-
def self.find(domain)
|
99
|
-
Domain.first(:domain_name => domain)
|
100
|
-
end
|
101
|
-
|
102
|
-
def self.exist?(domain)
|
103
|
-
!!Domain.find(domain)
|
104
|
-
end
|
105
|
-
|
106
|
-
def self.num_total_aliases
|
107
|
-
Alias.count - Mailbox.count
|
108
|
-
end
|
109
|
-
|
110
|
-
def num_total_aliases
|
111
|
-
aliases.count - mailboxes.count
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
class DomainAdmin
|
116
|
-
include ::DataMapper::Resource
|
117
|
-
property :created, DateTime, :default => DateTime.now
|
118
|
-
property :domain_name, String, :field => 'domain', :key => true
|
119
|
-
property :username, String, :key => true
|
120
|
-
|
121
|
-
belongs_to :domain, :model => 'Domain', :child_key => :domain_name
|
122
|
-
belongs_to :admin, :model => 'Admin', :child_key => :username
|
123
|
-
storage_names[:default] = 'domain_admins'
|
124
|
-
end
|
125
|
-
|
126
|
-
class Mailbox
|
127
|
-
include ::DataMapper::Resource
|
128
|
-
property :username, String, :key => true
|
129
|
-
property :name, String
|
130
|
-
property :domain_name, String, :field => 'domain'
|
131
|
-
property :password, String
|
132
|
-
property :maildir, String
|
133
|
-
property :quota, Integer
|
134
|
-
# property :local_part, String
|
135
|
-
property :created, DateTime, :default => DateTime.now
|
136
|
-
property :modified, DateTime, :default => DateTime.now
|
137
|
-
|
138
|
-
belongs_to :domain, :model => 'Domain', :child_key => :domain_name
|
139
|
-
|
140
|
-
storage_names[:default] = 'mailbox'
|
141
|
-
|
142
|
-
def self.find(username)
|
143
|
-
Mailbox.first(:username => username)
|
144
|
-
end
|
145
|
-
|
146
|
-
def self.exist?(username)
|
147
|
-
!!Mailbox.find(username)
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
class Alias
|
152
|
-
include ::DataMapper::Resource
|
153
|
-
property :address, String, :key => true
|
154
|
-
property :goto, Text
|
155
|
-
property :domain_name, String, :field => 'domain'
|
156
|
-
property :created, DateTime, :default => DateTime.now
|
157
|
-
property :modified, DateTime, :default => DateTime.now
|
158
|
-
|
159
|
-
belongs_to :domain, :model => 'Domain', :child_key => :domain_name
|
160
|
-
|
161
|
-
storage_names[:default] = 'alias'
|
162
|
-
|
163
|
-
def self.find(address)
|
164
|
-
Alias.first(:address => address)
|
165
|
-
end
|
166
|
-
|
167
|
-
def self.exist?(address)
|
168
|
-
!!Alias.find(address)
|
169
|
-
end
|
170
|
-
end
|
171
|
-
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'
|