--- /dev/null
+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 <domain>/<username> 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
--- /dev/null
+#!/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
--- /dev/null
+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
--- /dev/null
+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