From 51f027b01e242737956c3ab5aecdd322d6ceeeed Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Sun, 29 Sep 2013 21:27:24 -0400 Subject: [PATCH] Way too many changes to mention. The 'rm' mode works now. --- bin/mailshears | 14 +-- lib/common/davical_plugin.rb | 27 +++++ lib/common/dovecot_mailstore_plugin.rb | 2 +- ...xadmin_db.rb => postfixadmin_db_plugin.rb} | 46 ++++--- lib/common/roundcube_db_plugin.rb | 29 +++++ lib/common/runner.rb | 6 + lib/mailshears.rb | 3 +- lib/mv/mv_dummy_runner.rb | 2 + lib/mv/mv_runner.rb | 2 + lib/prune/prune_dummy_runner.rb | 2 + lib/prune/prune_runner.rb | 2 + lib/rm/plugins/agendav.rb | 10 ++ lib/rm/plugins/davical.rb | 11 ++ lib/rm/plugins/dovecot_mailstore.rb | 5 +- lib/rm/plugins/postfixadmin_db.rb | 113 ++++++++++++++++++ lib/rm/plugins/roundcube_db.rb | 42 +------ lib/rm/rm_dummy_runner.rb | 8 +- lib/rm/rm_plugin.rb | 7 ++ lib/rm/rm_runner.rb | 28 ++++- 19 files changed, 284 insertions(+), 75 deletions(-) rename lib/common/{postfixadmin_db.rb => postfixadmin_db_plugin.rb} (72%) create mode 100644 lib/rm/plugins/postfixadmin_db.rb diff --git a/bin/mailshears b/bin/mailshears index 09735ba..37d6601 100755 --- a/bin/mailshears +++ b/bin/mailshears @@ -57,6 +57,9 @@ cfg.plugins.each do |plugin_file| require "#{mode_name}/plugins/#{plugin_file}" end +# Always enabled, for now. +require "#{mode_name}/plugins/postfixadmin_db" + # And the runners. require "#{mode_name}/#{mode_name}_runner" require "#{mode_name}/#{mode_name}_dummy_runner" @@ -102,10 +105,10 @@ else dummy_runner_class = PruneDummyRunner end +puts make_header(plugin_class.to_s()) plugin_class.includers.each do |plugin_class_includer| plugin = plugin_class_includer.new() - puts make_header(plugin_class.to_s()) if cfg.i_mean_business then runner = runner_class.new() @@ -124,15 +127,6 @@ end Kernel.exit(0) -pgadb = PostfixadminDb.new(cfg.dbhost, - cfg.dbport, - cfg.dbopts, - cfg.dbtty, - cfg.dbname, - cfg.dbuser, - cfg.dbpass) - - begin diff --git a/lib/common/davical_plugin.rb b/lib/common/davical_plugin.rb index e4bef48..cdde3d1 100644 --- a/lib/common/davical_plugin.rb +++ b/lib/common/davical_plugin.rb @@ -70,4 +70,31 @@ module DavicalPlugin end + def get_davical_usernames() + usernames = [] + + begin + connection = PGconn.connect(@db_host, + @db_port, + @db_opts, + @db_tty, + @db_name, + @db_user, + @db_pass) + + sql_query = "SELECT username FROM usr" + + connection.query(sql_query) do |result| + usernames = result.field_values('username') + end + + connection.close() + rescue PGError => e + # Pretend like we're database-agnostic in case we ever are. + raise DatabaseError.new(e) + end + + return usernames + end + end diff --git a/lib/common/dovecot_mailstore_plugin.rb b/lib/common/dovecot_mailstore_plugin.rb index ff901ee..d0f8dd1 100644 --- a/lib/common/dovecot_mailstore_plugin.rb +++ b/lib/common/dovecot_mailstore_plugin.rb @@ -67,7 +67,7 @@ module DovecotMailstorePlugin if File.directory?(account_path) return account_path else - raise NonexistentAccountError(account) + raise NonexistentAccountError.new(account) end end diff --git a/lib/common/postfixadmin_db.rb b/lib/common/postfixadmin_db_plugin.rb similarity index 72% rename from lib/common/postfixadmin_db.rb rename to lib/common/postfixadmin_db_plugin.rb index 0249ea0..936672e 100644 --- a/lib/common/postfixadmin_db.rb +++ b/lib/common/postfixadmin_db_plugin.rb @@ -1,22 +1,33 @@ +require 'common/plugin' require 'pg' -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 +module PostfixadminDbPlugin + # Code that all PostfixadminDb plugins (Prune, Rm, Mv...) will + # share. That is, we implement the Plugin interface. + include Plugin + + def initialize() + + cfg = Configuration.new() + @db_host = cfg.dbhost + @db_port = cfg.dbport + @db_opts = cfg.dbopts + @db_tty = cfg.dbtty + @db_name = cfg.dbname + @db_user = cfg.dbuser + @db_pass = cfg.dbpass + end + + + def describe_account(account) + # There's no other unique identifier in PostfixAdmin + return account + end + + + def describe_domain(domain) + # There's no other unique identifier in PostfixAdmin + return domain end @@ -76,4 +87,5 @@ class PostfixadminDb return accounts end + end diff --git a/lib/common/roundcube_db_plugin.rb b/lib/common/roundcube_db_plugin.rb index 364b819..e4e9e7f 100644 --- a/lib/common/roundcube_db_plugin.rb +++ b/lib/common/roundcube_db_plugin.rb @@ -65,4 +65,33 @@ module RoundcubeDbPlugin return user_id end + + # Uses in both prune/rm. + def get_roundcube_usernames() + usernames = [] + + # 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 username FROM users;" + connection.query(sql_query) do |result| + usernames = result.field_values('username') + end + + connection.close() + rescue PGError => e + # Pretend like we're database-agnostic in case we ever are. + raise DatabaseError.new(e) + end + + return usernames + end + end diff --git a/lib/common/runner.rb b/lib/common/runner.rb index c48f89a..b9bb8c9 100644 --- a/lib/common/runner.rb +++ b/lib/common/runner.rb @@ -2,4 +2,10 @@ module Runner def run(plugin, targets) raise NotImplementedError end + + def report(plugin, msg) + print "#{plugin.class.to_s()} - " + puts msg + end + end diff --git a/lib/mailshears.rb b/lib/mailshears.rb index 5eff521..bda9f0d 100644 --- a/lib/mailshears.rb +++ b/lib/mailshears.rb @@ -5,5 +5,4 @@ require 'common/configuration' require 'common/errors' require 'common/exit_codes' -require 'common/postfixadmin_db' -require 'common/runner' + diff --git a/lib/mv/mv_dummy_runner.rb b/lib/mv/mv_dummy_runner.rb index 0e3beee..0d06fc1 100644 --- a/lib/mv/mv_dummy_runner.rb +++ b/lib/mv/mv_dummy_runner.rb @@ -1,3 +1,5 @@ +require 'common/runner' + class MvDummyRunner include Runner diff --git a/lib/mv/mv_runner.rb b/lib/mv/mv_runner.rb index a0fa8e7..c7dbc5e 100644 --- a/lib/mv/mv_runner.rb +++ b/lib/mv/mv_runner.rb @@ -1,3 +1,5 @@ +require 'common/runner' + class MvRunner include Runner diff --git a/lib/prune/prune_dummy_runner.rb b/lib/prune/prune_dummy_runner.rb index f9bde5e..8e9fcd9 100644 --- a/lib/prune/prune_dummy_runner.rb +++ b/lib/prune/prune_dummy_runner.rb @@ -1,3 +1,5 @@ +require 'common/runner' + class PruneDummyRunner include Runner diff --git a/lib/prune/prune_runner.rb b/lib/prune/prune_runner.rb index 0f1dfeb..789ed57 100644 --- a/lib/prune/prune_runner.rb +++ b/lib/prune/prune_runner.rb @@ -1,3 +1,5 @@ +require 'common/runner' + class PruneRunner include Runner diff --git a/lib/rm/plugins/agendav.rb b/lib/rm/plugins/agendav.rb index 4cd179c..6aa6d91 100644 --- a/lib/rm/plugins/agendav.rb +++ b/lib/rm/plugins/agendav.rb @@ -42,4 +42,14 @@ class AgendavRm end + + def get_domain_usernames(domain) + usernames = get_agendav_usernames(); + matches = usernames.select do |username| + username =~ /@#{domain}$/ + end + + return matches + end + end diff --git a/lib/rm/plugins/davical.rb b/lib/rm/plugins/davical.rb index 86273cb..ce5828a 100644 --- a/lib/rm/plugins/davical.rb +++ b/lib/rm/plugins/davical.rb @@ -45,4 +45,15 @@ class DavicalRm end + + + def get_domain_usernames(domain) + usernames = get_davical_usernames(); + matches = usernames.select do |username| + username =~ /@#{domain}$/ + end + + return matches + end + end diff --git a/lib/rm/plugins/dovecot_mailstore.rb b/lib/rm/plugins/dovecot_mailstore.rb index e138651..8ab507d 100644 --- a/lib/rm/plugins/dovecot_mailstore.rb +++ b/lib/rm/plugins/dovecot_mailstore.rb @@ -40,6 +40,10 @@ class DovecotMailstoreRm < Mailstore end + def get_domain_usernames(domain) + return get_accounts_from_filesystem([domain]) + end + protected; def get_domains_from_filesystem() @@ -68,5 +72,4 @@ class DovecotMailstoreRm < Mailstore return accounts end - end diff --git a/lib/rm/plugins/postfixadmin_db.rb b/lib/rm/plugins/postfixadmin_db.rb new file mode 100644 index 0000000..cd59928 --- /dev/null +++ b/lib/rm/plugins/postfixadmin_db.rb @@ -0,0 +1,113 @@ +require 'pg' + +require 'common/postfixadmin_db_plugin' +require 'rm/rm_plugin' + +class PostfixadminDbRm + + include PostfixadminDbPlugin + include RmPlugin + + + def delete_account(account) + sql_queries = ['DELETE FROM alias WHERE address = $1;'] + # Wipe out any aliases pointed at our account. + sql_queries << "UPDATE alias SET goto=REPLACE(goto, $1, '');" + sql_queries << 'DELETE FROM mailbox WHERE username = $1;' + sql_queries << 'DELETE FROM quota WHERE username = $1;' + sql_queries << 'DELETE FROM quota2 WHERE username = $1;' + sql_queries << 'DELETE FROM vacation WHERE email = $1;' + + # Should be handled by a trigger, according to PostfixAdmin code. + sql_queries << 'DELETE FROM vacation_notification WHERE on_vacation = $1;' + + begin + connection = PGconn.connect(@db_host, + @db_port, + @db_opts, + @db_tty, + @db_name, + @db_user, + @db_pass) + + sql_queries.each do |sql_query| + connection.query(sql_query, [account]) + end + + # The earlier alias update query will leave things like + # "foo@example.com,,bar@example.com" in the "goto" column. Now + # we fix it. We don't do it in the loop because query() craps + # out on the superfluous parameter. + sql_query = "UPDATE alias SET goto=REPLACE(goto, ',,', ',');" + connection.query(sql_query) + + connection.close() + + rescue PGError => e + # Pretend like we're database-agnostic in case we ever are. + raise DatabaseError.new(e) + end + end + + + def delete_domain(domain) + sql_queries = ['DELETE FROM domain_admins WHERE domain = $1;'] + sql_queries << 'DELETE FROM alias WHERE domain = $1;' + sql_queries << 'DELETE FROM mailbox WHERE domain = $1;' + sql_queries << 'DELETE FROM alias_domain WHERE alias_domain = $1;' + sql_queries << 'DELETE FROM log WHERE domain = $1;' + sql_queries << 'DELETE FROM vacation WHERE domain = $1;' + + begin + connection = PGconn.connect(@db_host, + @db_port, + @db_opts, + @db_tty, + @db_name, + @db_user, + @db_pass) + + sql_queries.each do |sql_query| + connection.query(sql_query, [domain]) + end + + connection.close() + + rescue PGError => e + # Pretend like we're database-agnostic in case we ever are. + raise DatabaseError.new(e) + end + + end + + + + def get_domain_usernames(domain) + usernames = [] + + # 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 username FROM mailbox WHERE domain = $1;' + + connection.query(sql_query, [domain]) do |result| + usernames = result.field_values('username') + end + + connection.close() + rescue PGError => e + # Pretend like we're database-agnostic in case we ever are. + raise DatabaseError.new(e) + end + + return usernames + end + +end diff --git a/lib/rm/plugins/roundcube_db.rb b/lib/rm/plugins/roundcube_db.rb index 3e18e8e..252ce32 100644 --- a/lib/rm/plugins/roundcube_db.rb +++ b/lib/rm/plugins/roundcube_db.rb @@ -51,46 +51,14 @@ class RoundcubeDbRm end - def get_leftover_domains(db_domains) - # Roundcube doesn't have a concept of domains. - return [] - end - - - def get_leftover_accounts(db_accounts) - # Get a list of all users who have logged in to Roundcube. - rc_accounts = self.get_roundcube_usernames() - return rc_accounts - db_accounts - end - - protected; - - def get_roundcube_usernames() - usernames = [] - - # 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 username FROM users;" - connection.query(sql_query) do |result| - usernames = result.field_values('username') - end - - connection.close() - rescue PGError => e - # Pretend like we're database-agnostic in case we ever are. - raise DatabaseError.new(e) + def get_domain_usernames(domain) + usernames = get_roundcube_usernames(); + matches = usernames.select do |username| + username =~ /@#{domain}$/ end - return usernames + return matches end end diff --git a/lib/rm/rm_dummy_runner.rb b/lib/rm/rm_dummy_runner.rb index d6c2c5f..9bdd13f 100644 --- a/lib/rm/rm_dummy_runner.rb +++ b/lib/rm/rm_dummy_runner.rb @@ -1,3 +1,5 @@ +require 'common/runner' + class RmDummyRunner include Runner @@ -5,12 +7,16 @@ class RmDummyRunner targets.each do |target| if target.include?('@') then puts "Would remove account: #{target}" + # TODO: remove from postfixadmin as well. else + usernames = plugin.get_domain_usernames(target) + usernames.each { |u| run(plugin, u) } + puts "Would remove domain: #{target}" + # TODO: remove from postfixadmin as well. end end - # TODO: remove from postfixadmin as well. end end diff --git a/lib/rm/rm_plugin.rb b/lib/rm/rm_plugin.rb index f8cdd17..cad493d 100644 --- a/lib/rm/rm_plugin.rb +++ b/lib/rm/rm_plugin.rb @@ -25,4 +25,11 @@ module RmPlugin raise NotImplementedError end + def get_domain_usernames(domain) + # Get a list of usernames for a given domain, + # needed to delete accounts before removing + # a domain. + raise NotImplementedError + end + end diff --git a/lib/rm/rm_runner.rb b/lib/rm/rm_runner.rb index 5fe2eaf..0392d11 100644 --- a/lib/rm/rm_runner.rb +++ b/lib/rm/rm_runner.rb @@ -1,3 +1,5 @@ +require 'common/runner' + class RmRunner include Runner @@ -8,25 +10,39 @@ class RmRunner begin account_description = plugin.describe_account(target) plugin.delete_account(target) - puts "Removed account: #{target} (#{account_description})" + report(plugin, "Removed account: #{target} (#{account_description})") + + rescue NonexistentAccountError => e + report(plugin, "Account not found: #{e.to_s}") rescue StandardError => e - puts "There was an error removing the account: #{e.to_s}" + report(plugin, "There was an error removing the account: #{e.to_s}") Kernel.exit(ExitCodes::DATABASE_ERROR) end else begin - # TODO: Delete all accounts first. + # We must delete all accounts belonging to the domain first. + # This prevents us from leaving behind accounts. Not a + # problem with the mailstore, since we'll delete the domain + # directory anyway, but it is for the database plugins. + usernames = plugin.get_domain_usernames(target) + usernames.each { |u| run(plugin, u) } + domain_description = plugin.describe_domain(target) plugin.delete_domain(target) - puts "Removed domain: #{target} (#{domain_description})" + report(plugin, "Removed domain: #{target} (#{domain_description})") + + rescue NonexistentAccountError => e + # Can happen in the usernames.each... block. + report(plugin, "Account not found: #{e.to_s}") + rescue NonexistentDomainError => e + report(plugin, "Domain not found: #{e.to_s}") rescue StandardError => e - puts "There was an error removing the domain: #{e.to_s}" + report(plugin, "There was an error removing the domain: #{e.to_s}") Kernel.exit(ExitCodes::DATABASE_ERROR) end end end - # TODO: remove from postfixadmin as well. end end -- 2.43.2