Initial commit.
authorMichael Orlitzky <michael@orlitzky.com>
Thu, 11 Feb 2010 00:33:32 +0000 (19:33 -0500)
committerMichael Orlitzky <michael@orlitzky.com>
Thu, 11 Feb 2010 00:33:32 +0000 (19:33 -0500)
bin/configuration.rb [new file with mode: 0644]
bin/mailshears [new file with mode: 0755]
src/dovecot_mailstore.rb [new file with mode: 0644]
src/errors.rb [new file with mode: 0644]
src/exit_codes.rb [new file with mode: 0644]
src/filesystem.rb [new file with mode: 0644]
src/mailstore.rb [new file with mode: 0644]
src/postfixadmin_db.rb [new file with mode: 0644]

diff --git a/bin/configuration.rb b/bin/configuration.rb
new file mode 100644 (file)
index 0000000..dcfa513
--- /dev/null
@@ -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 <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
diff --git a/bin/mailshears b/bin/mailshears
new file mode 100755 (executable)
index 0000000..fc20dd3
--- /dev/null
@@ -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 (file)
index 0000000..187b6b0
--- /dev/null
@@ -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 (file)
index 0000000..a930a71
--- /dev/null
@@ -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 (file)
index 0000000..f304ff6
--- /dev/null
@@ -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 (file)
index 0000000..473a1ed
--- /dev/null
@@ -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 (file)
index 0000000..4d75b87
--- /dev/null
@@ -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 (file)
index 0000000..e30c89c
--- /dev/null
@@ -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