From da71911046617ccffbb899b57162c5e6bdbb37ee Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 10 Feb 2010 19:33:32 -0500 Subject: [PATCH 1/1] Initial commit. --- bin/configuration.rb | 17 ++++++++++ bin/mailshears | 71 ++++++++++++++++++++++++++++++++++++++++ src/dovecot_mailstore.rb | 23 +++++++++++++ src/errors.rb | 4 +++ src/exit_codes.rb | 7 ++++ src/filesystem.rb | 22 +++++++++++++ src/mailstore.rb | 13 ++++++++ src/postfixadmin_db.rb | 45 +++++++++++++++++++++++++ 8 files changed, 202 insertions(+) create mode 100644 bin/configuration.rb create mode 100755 bin/mailshears create mode 100644 src/dovecot_mailstore.rb create mode 100644 src/errors.rb create mode 100644 src/exit_codes.rb create mode 100644 src/filesystem.rb create mode 100644 src/mailstore.rb create mode 100644 src/postfixadmin_db.rb diff --git a/bin/configuration.rb b/bin/configuration.rb new file mode 100644 index 0000000..dcfa513 --- /dev/null +++ b/bin/configuration.rb @@ -0,0 +1,17 @@ +module Configuration + # Where your mailboxes are stored. The exact format could + # theoretically change in the future, but for now, the + # DovecotMailstore class is going to assume that the mailboxes are + # stored beneath this directory in / format. + MAIL_ROOT = '/var/spool/mail/vhosts' + + # These should be obvious except for the ones that aren't. You can + # identify the non-obvious ones by my having left them blank. + DBHOST = 'localhost' + DBPORT = 5432 + DBOPTS = '' + DBTTY = '' + DBUSER = 'postgres' + DBPASS = '' + DBNAME = 'postfix' +end diff --git a/bin/mailshears b/bin/mailshears new file mode 100755 index 0000000..fc20dd3 --- /dev/null +++ b/bin/mailshears @@ -0,0 +1,71 @@ +#!/usr/bin/ruby -wKU +# +# mailshears, to prune unused mail directories. +# +# Mail accounts for virtual hosts are stored in SQL, and managed by +# Postfixadmin. However, the physical directories are handled by +# Postfix/Dovecot and are left untouched by Postfixadmin. This is good +# for security, but comes at a cost: Postfixadmin can't remove a +# user's mail directory when his or her account is deleted. +# +# This program compares the list of filesystem accounts with the ones +# in the database. It outputs any accounts that exist in the +# filesystem, but not the database. +# + +# We need Pathname to get the real filesystem path +# of this script (and not, for example, the path of +# a symlink which points to it. +require 'pathname' + +# This bit of magic adds the parent directory (the +# project root) to the list of ruby load paths. +# Thus, our require statements will work regardless of +# how or from where the script was run. +executable = Pathname.new(__FILE__).realpath.to_s +$: << File.dirname(executable) + '/../' + +# Load our config file. +require 'bin/configuration' + +# And the necessary classes. +require 'src/errors.rb' +require 'src/exit_codes.rb' +require 'src/dovecot_mailstore' +require 'src/postfixadmin_db' + +dms = DovecotMailstore.new(Configuration::MAIL_ROOT) + +pgadb = PostfixadminDb.new(Configuration::DBHOST, + Configuration::DBPORT, + Configuration::DBOPTS, + Configuration::DBTTY, + Configuration::DBNAME, + Configuration::DBUSER, + Configuration::DBPASS) + +begin + # Get the list of accounts according to the filesystem. + fs_accts = dms.get_accounts_from_filesystem() +rescue StandardError => e + puts "There was an error retrieving accounts from the filesystem: #{e.to_s}" + Kernel.exit(ExitCodes::FILESYSTEM_ERROR) +end + +begin + # ...and according to the Postfixadmin database. + db_accts = pgadb.get_accounts_from_db() +rescue DatabaseError => e + puts "There was an error connecting to the database: #{e.to_s}" + Kernel.exit(ExitCodes::DATABASE_ERROR) +end + + +# Figure out which addresses are in the filesystem, but not in the +# database. +difference = [fs_accts - db_accts] + +# Don't output any unnecessary junk. Cron might mail it to someone. +if difference.size > 0 + puts difference +end diff --git a/src/dovecot_mailstore.rb b/src/dovecot_mailstore.rb new file mode 100644 index 0000000..187b6b0 --- /dev/null +++ b/src/dovecot_mailstore.rb @@ -0,0 +1,23 @@ +require 'src/filesystem' +require 'src/mailstore' + +class DovecotMailstore < Mailstore + + def get_accounts_from_filesystem() + accounts = [] + + domains = Filesystem.get_subdirs(@domain_root) + + domains.each do |domain| + domain_path = File.join(@domain_root, domain) + usernames = Filesystem.get_subdirs(domain_path) + + usernames.each do |username| + accounts << "#{username}@#{domain}" + end + end + + return accounts + end + +end diff --git a/src/errors.rb b/src/errors.rb new file mode 100644 index 0000000..a930a71 --- /dev/null +++ b/src/errors.rb @@ -0,0 +1,4 @@ +# A generalization of PGError, and whatever MySQL and the other +# databases might eventually use. +class DatabaseError < StandardError +end diff --git a/src/exit_codes.rb b/src/exit_codes.rb new file mode 100644 index 0000000..f304ff6 --- /dev/null +++ b/src/exit_codes.rb @@ -0,0 +1,7 @@ +module ExitCodes + + SUCCESS = 0 + FILESYSTEM_ERROR = 1 + DATABASE_ERROR = 2 + +end diff --git a/src/filesystem.rb b/src/filesystem.rb new file mode 100644 index 0000000..473a1ed --- /dev/null +++ b/src/filesystem.rb @@ -0,0 +1,22 @@ +class Filesystem + + def self.begins_with_dot(path) + return (path[0..0] == '.') + end + + def self.get_subdirs(dir) + subdirs = [] + + Dir.open(dir) do |d| + d.each do |entry| + relative_path = File.join(dir, entry) + if (File.directory?(relative_path) and not begins_with_dot(entry)) + subdirs << entry + end + end + end + + return subdirs + end + +end diff --git a/src/mailstore.rb b/src/mailstore.rb new file mode 100644 index 0000000..4d75b87 --- /dev/null +++ b/src/mailstore.rb @@ -0,0 +1,13 @@ +class Mailstore + + attr_accessor :domain_root + + def initialize(domain_root) + @domain_root = domain_root + end + + def get_accounts_from_filesystem() + raise NotImplementedError + end + +end diff --git a/src/postfixadmin_db.rb b/src/postfixadmin_db.rb new file mode 100644 index 0000000..e30c89c --- /dev/null +++ b/src/postfixadmin_db.rb @@ -0,0 +1,45 @@ +require 'postgres' + +class PostfixadminDb + + def initialize(db_host, + db_port, + db_opts, + db_tty, + db_name, + db_user, + db_pass) + + @db_host = db_host + @db_port = db_port + @db_opts = db_opts + @db_tty = db_tty + @db_name = db_name + @db_user = db_user + @db_pass = db_pass + end + + + def get_accounts_from_db() + # Just assume PostgreSQL for now. + begin + connection = PGconn.connect(@db_host, + @db_port, + @db_opts, + @db_tty, + @db_name, + @db_user, + @db_pass) + + sql_query = 'SELECT address FROM alias;' + result = connection.query(sql_query) + connection.close() + rescue PGError => e + # But pretend like we're database-agnostic in case we ever are. + raise DatabaseError.new(e) + end + + return result + end + +end -- 2.44.2