require 'mailshears'
# Define a usage string using the program name.
-exe = File.basename($PROGRAM_NAME)
-usage = "#{exe} [prune | rm <target> | mv <src> <dst>]"
+program_name = File.basename($PROGRAM_NAME)
# Defaults
mode_name = 'prune'
# here. Report it and exit with a failure code.
if not args_error_message.nil? then
STDERR.puts args_error_message
- puts "Usage: #{usage}"
+ puts "Usage: #{UserInterface.usage(program_name)}"
Kernel.exit(ExitCodes::BAD_COMMAND_LINE)
end
$stdout = STDOUT
if output_buffer.size > 0 then
- puts make_header(exe, plugin_module.to_s())
+ puts UserInterface.make_header(program_name, plugin_module.to_s())
puts output_buffer.string()
end
end
require 'common/plugin'
require 'common/user'
+# Code that all Agendav plugins ({AgendavPrune}, {AgendavRm},
+# {AgendavMv}) share.
module AgendavPlugin
- # Code that all Agendav plugins (Prune, Rm, Mv...) will
- # share. That is, we implement the Plugin interface.
+
+ # We implement the Plugin "interface."
include Plugin
+ # Initialize this Agendav {Plugin} with values in *cfg*.
+ #
+ # @param cfg [Configuration] the configuration for this plugin.
+ #
def initialize(cfg)
@db_host = cfg.agendav_dbhost
@db_port = cfg.agendav_dbport
end
+ # Return a list of Agendav users.
+ #
+ # @return [Array<User>] a list of users contained in the
+ # Agendav database.
+ #
def list_users()
- #
- # Produce a list of AgenDAV users. This is public because it's
- # useful in testing.
- #
users = []
connection = PGconn.connect(@db_host, @db_port, @db_opts, @db_tty,
require 'yaml'
+# A configuration object that knows how to read options out of a file
+# in <tt>~/.mailshears.conf.yml</tt>. The configuration options can be
+# accessed via methods even though the internal representation is a
+# hash.
+#
+# === Examples
+#
+# >> cfg = Configuration.new()
+# >> cfg.i_mean_business()
+# => true
+#
class Configuration
+ # The default path to the user's configuration file.
USERCONF_PATH = ENV['HOME'] + '/.mailshears.conf.yml'
+
+ # The hash structure in which we store our configuration options
+ # internally.
@dict = {}
+
+ # Initialize a {Configuration} object with the config file at *path*.
+ #
+ # @param path [String] the path to the configuration file to
+ # load. We check for a file named ".mailshears.conf.yml" in the
+ # user's home directory by default.
+ #
def initialize(path = USERCONF_PATH)
cfg = default_configuration()
end
+ # Replace all missing method calls with hash lookups. This lets us
+ # retrieve the values in our option hash by using methods named
+ # after the associated keys.
+ #
+ # @param sym [Symbol] the method that was called.
+ #
+ # @return [Object] the config file value associated with *sym*.
+ #
def method_missing(sym, *args)
- # Replace all missing method calls with dictionary lookups.
return @dict[sym]
end
private;
+
+ # A default config hash.
+ #
+ # @return [Hash] sensible default configuration values.
+ #
def default_configuration()
d = {}
require 'common/plugin'
require 'common/user'
+# Code that all DAViCal plugins ({DavicalPrune}, {DavicalRm}, and
+# {DavicalMv}) will share.
+#
module DavicalPlugin
- # Code that all Davical plugins (Prune, Rm, Mv...) will share. That
- # is, we implement the Plugin interface.
+
+ # We implement the Plugin "interface."
include Plugin
+ # Initialize this DAViCal {Plugin} with values in *cfg*.
+ #
+ # @param cfg [Configuration] the configuration for this plugin.
+ #
def initialize(cfg)
@db_host = cfg.davical_dbhost
@db_port = cfg.davical_dbport
end
+ # Describe the given DAViCal user who is assumed to exist.
+ #
+ # @param user [User] the {User} object whose description we want.
+ #
+ # @return [String] a String describing the given *user* in terms
+ # of his DAViCal "Principal ID".
+ #
def describe_user(user)
principal_id = self.get_principal_id(user)
return "Principal ID: #{principal_id}"
end
+ #
+ # Produce a list of DAViCal users.
+ #
+ # This method remains public for use in testing.
+ #
+ # @return [Array<User>] an array of {User} objects, one for each
+ # user found in the DAViCal database.
+ #
def list_users()
- #
- # Produce a list of DAViCal users. This is public because it's
- # useful for testing.
- #
usernames = []
connection = PGconn.connect(@db_host, @db_port, @db_opts, @db_tty,
protected;
+
+ # Find the "Principal ID" of the given user.
+ #
+ # @param user [User] the user whose Principal ID we want.
+ #
+ # @return [Fixnum] an integer representing the user's Principal ID
+ # that we obtained from the DAViCal database.
+ #
def get_principal_id(user)
principal_id = nil
require 'common/errors'
+# A class representing a syntactically valid domain. Essentially, the
+# part after the "@" in an email address. Once constructed, you can be
+# sure that it's valid.
+#
class Domain
+ # @domain contains the String representation of this domain.
@domain = nil
+
+ # Initialize this Domain object. If the domain is invalid, then an
+ # {InvalidDomainError} will be raised containing the reason
+ # why the domain is invalid.
+ #
+ # @param domain [String] the string representation of this domain.
+ #
def initialize(domain)
if domain.empty? then
msg = "domain cannot be empty"
@domain = domain
end
+
+ # Convert this domain to a String.
+ #
+ # @return [String] the string representation of this Domain.
+ #
def to_s()
return @domain
end
+
+ # Check if this Domain is equal to some other Domain. The comparison
+ # is based on their String representations.
+ #
+ # @param other [Domain] the Domain object to compare me to.
+ #
+ # @return [Boolean] If *self* and *other* have equal String
+ # representations, then true is returned. Otherwise, false is
+ # returned.
+ #
def ==(other)
return self.to_s() == other.to_s()
end
+
+ # Compare two Domain objects for sorting purposes. The comparison
+ # is based on their String representations.
+ #
+ # @param other [Domain] the Domain object to compare me to.
+ #
+ # @return [0,1,-1] a trinary indicator of how *self* relates to *other*,
+ # obtained by performing the same comparison on the String
+ # representations of *self* and *other*.
+ #
def <=>(other)
return self.to_s() <=> other.to_s()
end
require 'common/plugin'
require 'common/user'
-
+# Code that all Dovecot plugins ({DovecotPrune}, {DovecotRm}, and
+# {DovecotMv}) will share.
+#
module DovecotPlugin
- # Code that all Dovecot plugins (Prune, Rm, Mv...) will
- # share. That is, we implement the Plugin interface.
+
+ # We implement the Plugin "interface."
include Plugin
+
+ # Initialize this Dovecot {Plugin} with values in *cfg*.
+ #
+ # @param cfg [Configuration] the configuration for this plugin.
+ #
def initialize(cfg)
@domain_root = cfg.dovecot_mail_root
end
+ # Describe the given Dovecot domain by its filesystem path. The
+ # domain need not exist to obtain its path.
+ #
+ # @param domain [Domain] the {Domain} object whose description we want.
+ #
+ # @return [String] a String giving the path under which this domain's
+ # mailboxes would reside on the filesystem.
+ #
def describe_domain(domain)
return get_domain_path(domain)
end
+
+ # Describe the given Dovecot user by its filesystem mailbox
+ # path. The user need not exist to obtain its mailbox path.
+ #
+ # @param user [User] the {User} object whose description we want.
+ #
+ # @return [String] a String giving the path where this user's
+ # mailbox would reside on the filesystem.
+ #
def describe_user(user)
return get_user_path(user)
end
protected;
+ # Return the filesystem path for the given {Domain} object.
+ #
+ # @param domain [Domain] the {Domain} whose path we want.
+ #
+ # @return [String] the filesystem path where this domain's mail
+ # would be located.
+ #
def get_domain_path(domain)
- # Return the filesystem path for the given domain.
- # That is, the directory where its mail is stored.
- # Only works if the domain directory exists!
return File.join(@domain_root, domain.to_s())
end
+ # Return the filesystem path of this {User}'s mailbox.
+ #
+ # @param user [User] the {User} whose mailbox path we want.
+ #
+ # @return [String] the filesystem path where this user's mail
+ # would be located.
+ #
def get_user_path(user)
- # Return the filesystem path of this user's mailbox.
domain_path = get_domain_path(user.domain())
return File.join(domain_path, user.localpart())
end
+ # Produce a list of domains that exist in the Dovecot mailstore.
+ #
+ # @return [Array<Domain>] an array of {Domain} objects that have
+ # corresponding directories within the Dovecot mailstore.
+ #
def list_domains()
return Filesystem.get_subdirs(@domain_root).map{ |d| Domain.new(d) }
end
+
+ # Produce a list of users belonging to the given *domains* in the
+ # Dovecot mailstore.
+ #
+ # @param domains [Array<Domain>] an array of {Domain} objects whose
+ # users we'd like to find.
+ #
+ # @return [Array<User>] an array of {User} objects that have
+ # corresponding directories within the Dovecot mailstore belonging
+ # to the specified *domains*.
+ #
def list_domains_users(domains)
users = []
end
+ # Produce a list of all users in the Dovecot mailstore.
+ #
+ # @return [Array<User>] a list of users who have mailbox directories
+ # within the Dovecot mailstore.
+ #
def list_users()
domains = list_domains()
users = list_domains_users(domains)
+# Command-line exit codes. In other words, what you'll see if you
+# <tt>echo $?</tt> after running the executable on the command-line.
module ExitCodes
# Everything went better than expected.
SUCCESS = 0
+# Convenience methods for working with the filesystem. This class
+# only provides static methods, to be used analogously to the File
+# class (for example, <tt>File.directory?</tt>).
+#
class Filesystem
- # Convenience methods for working with the filesystem. This class
- # only provides static methods, to be used analogously to the File
- # class (for example, File.directory?).
-
# Return whether or not the given path begins with a dot (ASCII
# period).
#
def self.get_subdirs(dir)
subdirs = []
+ return subdirs if not File.directory?(dir)
Dir.open(dir) do |d|
d.each do |entry|
require 'common/domain'
require 'common/user'
-# All plugins should include this module. It defines the basic
-# operations that all plugins are supposed to support.
+# Methods that all plugins must provide. Certain operations -- for
+# example, user listing -- must be supported by all plugins. These
+# operations are defined here, often with naive default
+# implementations, but it is up to each individual plugin to ensure
+# that they are in fact implemented (well).
+#
module Plugin
+ # These are class methods for runnable plugins, meant to be
+ # _extended_. Those runnable plugins get a magic *run* method but
+ # need to define their own *runner* and *dummy_runner* to make it
+ # work.
+ #
module Run
- # Module methods, meant to be extended. Includers can explicitly
- # extend this to get a run() method defined in terms of their own
- # runner() and dummy_runner() methods.
+ # A callback function, called whenever another class or module
+ # includes this one. This is used to build a list of all things
+ # that inherited this class. Having such a list lets us run a
+ # collection of plugins without knowing in advance what they are.
+ #
+ # @param c [Class,Module] the name of the class or module that
+ # included us.
+ #
def included(c)
- # Callback, called whenever another class or module includes this
- # one. The parameter given is the name of the class or module
- # that included us.
+ puts c.class().to_s()
@includers ||= []
@includers << c
end
- def includers
+
+ # Obtain the list of classes and modules that have included this one.
+ #
+ # @return [Array<Class,Module>] the list of classes and modules
+ # that have included this one.
+ #
+ def includers()
@includers ||= []
return @includers
end
+
+ # The runner class associated with this plugin. This method must
+ # be supplied by the child class, since they will all have
+ # different runners.
+ #
+ # @return [Class] the runner class associated with this plugin.
+ #
def runner()
- # The Runner associated with this plugin.
raise NotImplementedError
end
+
+ # The "dummy" runner class associated with this plugin. This method
+ # must be supplied by the child class, since they will all have
+ # different dummy runners.
+ #
+ # @return [Class] the dummy runner class associated with this
+ # plugin.
+ #
def dummy_runner()
- # The DummyRunner associated with this plugin.
raise NotImplementedError
end
+
+ # Run all of the plugins that have included this module.
+ #
+ # @param cfg [Configuration] the configuration options to pass to
+ # each of the plugins.
+ #
+ # @param args [Array<Object>] a variable number of additional
+ # arguments to be passed to the plugins we're running.
+ #
def run(cfg, *args)
includers().each do |includer|
plugin = includer.new(cfg)
end
end
+
+ # A generic version of {#describe_user}/{#describe_domain} that
+ # dispatches base on the class of the target.
+ #
+ # @param target [User,Domain] either a user or a domain to describe.
+ #
+ # @return [String] a string describing the *target*. The contents of
+ # the string depend on the plugin.
+ #
def describe(target)
- # A generic version of describe_user/describe_domain that
- # dispatches base on the class of the target.
if target.is_a?(User)
if user_exists(target) then
return describe_user(target)
end
end
+
+ # Provide a description of the given *domain*. This is output along
+ # with the domain name and can be anything of use to the system
+ # administrator. The default doesn't do anything useful and should
+ # be overridden if possible.
+ #
+ # @param domain [Domain] the domain to describe.
+ #
+ # @return [String] a string description of *domain*.
+ #
def describe_domain(domain)
- # Provide a "description" of the domain. This is output along with
- # the domain name and can be anything of use to the system
- # administrator. The default doesn't do anything useful and should
- # be overridden.
return domain.to_s()
end
+
+ # Provide a description of the given *user*. This is output along
+ # with the username and can be anything of use to the system
+ # administrator. The default doesn't do anything useful and should
+ # be overridden if possible.
+ #
+ # @param user [User] the domain to describe.
+ #
+ # @return [String] a string description of *user*.
+ #
def describe_user(user)
- # Provide a "description" of the user. This is output along
- # with the domain name and can be anything of use to the system
- # administrator. The default doesn't do anything useful and should
- # be overridden.
return user.to_s()
end
+
+ # Return a list of all users managed by this plugin. This must be
+ # supplied by the individual plugins (who know how to find their
+ # users).
+ #
+ # @return [Array<User>] a list of the users that this plugin knows
+ # about.
+ #
def list_users()
- # Return a list of all users managed by this plugin.
raise NotImplementedError
end
+
+ # Return a list of all domains managed by this plugin. This must be
+ # supplied by the individual plugins (who know how to find their
+ # domains). Many plugins will not have a separate concept of
+ # "domain", so the default implementation constructs a list of
+ # domains resulting from {#list_users}.
+ #
+ # For plugins that do know about domains, smarter implementations
+ # are surely possible.
+ #
+ # @return [Array<Domain>] a list of the domains that this plugin knows
+ # about.
+ #
def list_domains()
- # Compute the domains from a list of users. Obviously much worse
- # than getting the domains the "smart" way, if such a way exists.
users = list_users()
domains = users.map{ |u| u.domain() }
return domains.uniq()
end
+
+ # Does the given *user* exist for this plugin? We use a naive
+ # implementation here based on {#list_users}. Plugins should override
+ # this with something faster.
+ #
+ # @param user [User] the user whose existence is in question.
+ #
+ # @return [Boolean] true if *user* exists for this plugin, and
+ # false otherwise.
+ #
def user_exists(user)
- # Does the given username exist for this plugin? We use a naive
- # implementation here based on list_users() which is required to
- # exist above. Plugins can override this with something fast.
users = list_users()
return users.include?(user)
end
+
+ # Does the given *domain* exist for this plugin? We use a naive
+ # implementation here based on {#list_domains}. Plugins that know
+ # about domains should override this with something fast.
+ #
+ # @param domain [Domain] the domain whose existence is in question.
+ #
+ # @return [Boolean] true if *domain* exists for this plugin, and
+ # false otherwise.
+ #
def domain_exists(domain)
- # Does the given domain exist for this plugin? We use a naive
- # implementation here based on list_domains() which is required to
- # exist above. Plugins can override this with something fast.
domains = list_domains()
return domains.include?(domain)
end
+
+ # List all users belonging to the given domains. We say that a user
+ # belongs to a domain "example.com" if the domain part of the user's
+ # email address is "example.com".
+ #
+ # This uses a naive loop, but relies only on the existence of
+ # {#list_users}. Plugins that know about domains should provide a
+ # more efficient implementation.
+ #
+ # @param domains [Array<Domain>] the domains whose users we want.
+ #
+ # @return [Array<User>] a list of {User} objects belonging to
+ # *domains* for this plugin.
+ #
def list_domains_users(domains)
- # Get all users belonging to the given domains. If a user has
- # domainpart "example.com" then it belongs to the domain
- # "example.com".
- #
- # This uses a naive loop, but relies only on the existence of a
- # list_users() method which is guaranteed above. More efficient
- # implementations can usually be made within the plugin.
domains_users = []
users = list_users();
require 'common/user'
require 'pg'
+# Code that all Postfixadmin plugins ({PostfixadminPrune},
+# {PostfixadminRm}, {PostfixadminMv}) share.
+#
module PostfixadminPlugin
- # Code that all Postfixadmin plugins (Prune, Rm, Mv...) will
- # share. That is, we implement the Plugin interface.
+
+ # We implement the Plugin "interface."
include Plugin
+ # Initialize this Postfixadmin {Plugin} with values in *cfg*.
+ #
+ # @param cfg [Configuration] the configuration for this plugin.
+ #
def initialize(cfg)
@db_host = cfg.postfixadmin_dbhost
@db_port = cfg.postfixadmin_dbport
end
+ # Obtain a list of domains from Postfixadmin. This is more efficient
+ # than the {Plugin} default implementation because domains have
+ # their own table in the database and we can easily select them
+ # rather than filtering the list of users.
+ #
+ # @return [Array<Domain>] a list of the domains in Postfixadmin.
+ #
def list_domains()
domains = []
end
-
+ # Return a list of Postfixadmin users.
+ #
+ # @return [Array<User>] a list of users contained in the
+ # Postfixadmin database.
+ #
def list_users()
users = []
end
+
+ # Efficiently list all Postfixadmin users belonging to the given
+ # Postfixadmin *domains*.
+ #
+ # @param domains [Array<Domain>] the domains whose users we want.
+ #
+ # @return [Array<User>] a list of {User} objects belonging to
+ # *domains* for this plugin.
+ #
def list_domains_users(domains)
usernames = []
+ return usernames if domains.length() == 0
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 IN $1;'
+ # The number of parameters that we'll pass into our prepared query
+ # is the number of domains that we're given. It's important that
+ # we have at least one domain here.
+ params = 1.upto(domains.length()).map{ |i| '$' + i.to_s() }.join(',')
+ sql_query = "SELECT username FROM mailbox WHERE domain IN (#{params});"
- connection.query(sql_query, domains.map{|d| d.to_s()}) do |result|
+ # Now replace each Domain with its string representation and pass
+ # those in as our individual parameters.
+ connection.query(sql_query, domains.map{ |d| d.to_s() }) do |result|
usernames = result.field_values('username')
end
end
+ # Get a list of all Postfixadmin aliases as a <tt>from => to</tt>
+ # hash. This is useful for testing, since aliases should be removed
+ # when either the "from user" or "to user" are removed.
+ #
+ # @return [Hash] all aliases known to Postfixadmin in the form of a
+ # <tt>from => to</tt> hash.
+ #
def list_aliases()
- #
- # Get a list of all aliases, useful for testing.
- #
aliases = []
connection = PGconn.connect(@db_host, @db_port, @db_opts, @db_tty,
sql_query = 'SELECT address,goto FROM alias;'
results = connection.query(sql_query)
results.each do |row|
- aliases << row # row should be a hash
+ # row should be a hash
+ aliases << row
end
connection.close()
return aliases
end
+
+ # A fast implementation of the "does this domain exist?"
+ # operation. It only queries the database for the existence of
+ # *domain* rather than a list of all domains (which is the default
+ # implementation).
+ #
+ # @param domain [Domain] the domain whose existence is in question.
+ #
+ # @return [Boolean] true if *domain* exists in the Postfixadmin
+ # database and false otherwise.
+ #
+ def domain_exists(domain)
+ count = 0
+
+ connection = PGconn.connect(@db_host, @db_port, @db_opts, @db_tty,
+ @db_name, @db_user, @db_pass)
+
+ sql_query = 'SELECT COUNT(domain) as count FROM domain WHERE domain = $1;'
+ connection.query(sql_query, [domain.to_s()]) do |result|
+ return false if result.ntuples() < 1
+ count = result.getvalue(0,0).to_i()
+
+ return false if count.nil?
+ end
+
+ connection.close()
+
+ return (count > 0)
+ end
+
end
require 'common/plugin'
require 'common/user'
+# Code that all Roundcube plugins ({RoundcubePrune}, {RoundcubeRm},
+# {RoundcubeMv}) share.
+#
module RoundcubePlugin
- # Code that all Roundcube plugins (Prune, Rm, Mv...) will share.
- # That is, we implement the Plugin interface.
+
+ # We implement the Plugin "interface."
include Plugin
+
+ # Initialize this Roundcube {Plugin} with values in *cfg*.
+ #
+ # @param cfg [Configuration] the configuration for this plugin.
+ #
def initialize(cfg)
@db_host = cfg.roundcube_dbhost
@db_port = cfg.roundcube_dbport
end
+ # Describe the given Roundcube *user*.
+ #
+ # @param user [User] the user whose description we want.
+ #
+ # @return [String] a string containing the Roundcube "User ID"
+ # associated with *user*.
+ #
def describe_user(user)
user_id = self.get_user_id(user)
return "User ID: #{user_id}"
end
+ # Return a list of Roundcube users.
+ #
+ # @return [Array<User>] a list of users contained in the
+ # Roundcube database.
+ #
def list_users()
- # Produce a list of Roundcube users. This is used in prune/rm, and
- # is public because it is useful in testing.
usernames = []
connection = PGconn.connect(@db_host, @db_port, @db_opts, @db_tty,
protected;
+
+ # Find the Roundcube "User ID" associated with the given *user*.
+ #
+ # @param user [User] the user whose Roundcube "User ID" we want.
+ #
+ # @return [Fixnum] the Roundcube "User ID" for *user*.
+ #
def get_user_id(user)
user_id = nil
+# Methods inherited by the various runner classes ({PruneRunner},
+# {MvRunner}, {RmRunner}).
+#
module Runner
- def run(cfg, plugin, targets)
+
+ # The main thing a runner does is <tt>run()</tt>. Each runner will
+ # actually take a different number of arguments, so their
+ # <tt>run()</tt> signatures will differ. This stub is only here to
+ # let you know that it needs to be implemented.
+ #
+ # @param args [Array<Object>] whatever arguments the real implementation
+ # would take.
+ #
+ def run(*args)
raise NotImplementedError
end
+
+ # Report a message from the given *plugin*. All this does is prefix
+ # the message with the plugin name and then print it to stdout.
+ #
+ # @param plugin [Object] t plugin object that generated the message
+ # we're reporting.
+ #
+ # @param msg [String] the message to report.
+ #
def report(plugin, msg)
print "#{plugin.class.to_s()} - "
puts msg
require 'common/domain'
require 'common/errors'
+# A class representing a syntactically valid user; that is, an email
+# address. Once constructed, you can be sure that it's valid.
+#
class User
- # A class representing a syntactically valid user; that is, an email
- # address. Once constructed, you can be sure that it's valid.
+ # @localpart is a String containing the "user" part of "user@domain".
@localpart = nil
+
+ # @domain contains a {Domain} object representing the domain part of
+ # "user@domain".
@domain = nil
+
+ # Obtain the {Domain} object corresponding to this User.
+ #
+ # @return [Domain] the domain corresponding to this User.
+ #
def domain()
return @domain
end
+
+ # Obtain the domain part of this User object as a string.
+ #
+ # @return [String] a String representation of this User's domain.
+ #
def domainpart()
return @domain.to_s()
end
+
+ # Initialize this User object. If either of the local/domain parts
+ # is invalid, then either an {InvalidUserError} or an {InvalidDomainError}
+ # will be raised containing the reason why that part is invalid.
+ #
+ # @param username [String] an email address from which to construct
+ # this User.
+ #
def initialize(username)
- # Initialize this user object, but throw an error if either the
- # localpart or domainpart are invalid. The one argument is an
- # email address string.
+
if not username.is_a?(String)
msg = 'username must be a String '
msg += "but a #{username.class.to_s()} was given"
@domain = Domain.new(parts[1])
end
+
+ # Obtain the "user" part of this User object as a String.
+ #
+ # @return [String] the "user" part of this User's "user@domain"
+ # address.
+ #
def localpart()
return @localpart
end
+
+ # Convert this User to an email address string.
+ #
+ # @return [String] an email address String constructed from this
+ # user's local and domain parts.
+ #
def to_s()
return @localpart + '@' + @domain.to_s()
end
+
+ # Check if this User is equal to some other User. The comparison
+ # is based on their String representations.
+ #
+ # @param other [User] the User object to compare me to.
+ #
+ # @return [Boolean] If *self* and *other* have equal String
+ # representations, then true is returned. Otherwise, false is
+ # returned.
+ #
def ==(other)
return self.to_s() == other.to_s()
end
+
+ # Compare two User objects for sorting purposes. The comparison is
+ # is based on their String representations.
+ #
+ # @param other [User] the User object to compare me to.
+ #
+ # @return [0,1,-1] a trinary indicator of how *self* relates to *other*,
+ # obtained by performing the same comparison on the String
+ # representations of *self* and *other*.
+ #
def <=>(other)
return self.to_s() <=> other.to_s()
end
-def make_header(program_name, plugin_name)
- # The header that we output before the list of domains/users.
- # Just the path of this script, the current time, and the plugin name.
- header = "#{program_name}, "
-
- current_time = Time.now()
- if current_time.respond_to?(:iso8601)
- # Somehow this method is missing on some machines.
- header += current_time.iso8601.to_s
- else
- # Fall back to whatever this looks like.
- header += current_time.to_s
+# Class methods for the creation and manipulation of our command-line
+# user interface.
+#
+class UserInterface
+
+ # Construct a usage string showing how to invoke the program.
+ #
+ # @param program_name [String] the name of this program, used to
+ # construct the usage string.
+ #
+ # @return [String] a string showing the format of a correct program
+ # invocation.
+ #
+ def self.usage(program_name)
+ return "#{program_name} [prune | rm <target> | mv <src> <dst>]"
+ end
+
+
+ # Construct the header that precedes our other output. An example is,
+ #
+ # mailshears, 2015-11-06 09:57:06 -0500 (Plugin: PrunePlugin)
+ # ------------------------------------------------------------
+ #
+ # @param program_name [String] the name of this program, to appear
+ # in the header.
+ #
+ # @param plugin_name [String] the name of the mode (prune, mv, etc.)
+ # plugin that is being run.
+ #
+ # @return [String] a string containing the output header.
+ #
+ def self.make_header(program_name, plugin_name)
+ header = "#{program_name}, "
+
+ current_time = Time.now()
+ if current_time.respond_to?(:iso8601)
+ # Somehow this method is missing on some machines.
+ header += current_time.iso8601.to_s()
+ else
+ # Fall back to whatever this looks like.
+ header += current_time.to_s()
+ end
+
+ header += ' (Plugin: ' + plugin_name + ")\n"
+ header += '-' * header.size() # Underline the header.
+
+ return header
end
- header += ' (Plugin: ' + plugin_name + ")\n"
- header += '-' * header.size # Underline the header.
- return header
end
require 'common/runner'
+# Dummy implementation of a {MvRunner}. Its <tt>run()</tt> method will
+# tell you what would have been moved, but will not actually perform
+# the operation.
+#
class MvDummyRunner
include Runner
+ # Pretend to move *src* to *dst* with *plugin*. Some "what if"
+ # information will be output to stdout. This is useful to see if
+ # there would be (for example) a username collision at *dst* before
+ # attempting the move in earnest.
+ #
+ # @param cfg [Configuration] the configuration options to pass to
+ # the *plugin* we're runnning.
+ #
+ # @param plugin [Class] plugin class that will perform the move.
+ #
+ # @param src [User] the source user to be moved.
+ #
+ # @param dst [User] the destination user, to which we will move *src*.
+ #
def run(cfg, plugin, src, dst)
if src.is_a?(Domain) or dst.is_a?(Domain) then
+require 'common/plugin.rb'
+
+# Plugins for moving (renaming) users. Moving domains is not supported.
+#
module MvPlugin
- #
- # Plugins for moving (renaming) users.
- #
+ # Absorb the subclass run() magic from the Plugin::Run module.
extend Plugin::Run
+ # The runner class associated with move plugins.
+ #
+ # @return [Class] the {MvRunner} class.
+ #
def self.runner()
return MvRunner
end
+
+ # The "dummy" runner class associated with move plugins.
+ #
+ # @return [Class] the {MvDummyRunner} class.
+ #
def self.dummy_runner()
return MvDummyRunner
end
+
+ # The interface for the "move a user" operation. Subclasses need to
+ # implement this method so that it moves (renames) the user *src* to
+ # the user *dst*.
+ #
+ # @param src [User] the source user to be moved.
+ #
+ # @param dst [User] the destination user to which we'll move *src*.
+ #
def mv_user(src, dst)
- # Rename the given user.
raise NotImplementedError
end
require 'common/errors'
require 'common/runner'
+# Perform the moving (renaming) of users/domains using {MvPlugin}s.
+#
class MvRunner
include Runner
+ # Run *plugin* to move the user *src* to *dst*. The method
+ # signature includes the unused *cfg* for consistency with the
+ # runners that do need a {Configuration}.
+ #
+ # @param cfg [Configuration] unused.
+ #
+ # @param plugin [Class] plugin class that will perform the move.
+ #
+ # @param src [User] the source user to be moved.
+ #
+ # @param dst [User] the destination user being moved to.
+ #
def run(cfg, plugin, src, dst)
if src.is_a?(Domain) or dst.is_a?(Domain) then
require 'common/agendav_plugin'
require 'mv/mv_plugin'
+
+# Handle moving (renaming) Agendav users in its database. Agendav has
+# no concept of domains.
+#
class AgendavMv
include AgendavPlugin
include MvPlugin
+ # Move the user *src* to *dst* within the Agendav database. This
+ # should "rename" him in _every_ table where he is referenced.
+ #
+ # This can fail is *src* does not exist, or if *dst* already exists
+ # before the move. It should also be an error if the destination
+ # domain doesn't exist. But Agendav doesn't know about domains, so
+ # we let that slide.
+ #
+ # @param src [User] the source user to be moved.
+ #
+ # @param dst [User] the destination user being moved to.
+ #
def mv_user(src, dst)
- # It's obviously an error if the source user does not exist. It
- # would also be an error if the destination domain didn't exist;
- # however, Agendav doesn't know about domains, so we let that slide.
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
-
- # And it's an error if the destination user exists already.
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
sql_queries = ['UPDATE prefs SET username = $1 WHERE username = $2;']
require 'common/davical_plugin'
require 'rm/rm_plugin'
+# Handle moving (renaming) DAViCal users in its database. DAViCal has
+# no concept of domains.
+#
class DavicalMv
- #
- # DAViCal only supports Postgres, so even if we ever are
- # database-agnostic, this plugin can't be.
- #
include DavicalPlugin
include MvPlugin
+ # Move the user *src* to *dst* within the DAViCal database. This
+ # should "rename" him in _every_ table where he is referenced.
+ # DAViCal uses foreign keys properly, so we let the ON UPDATE
+ # CASCADE trigger handle most of the work.
+ #
+ # This can fail is *src* does not exist, or if *dst* already exists
+ # before the move. It should also be an error if the destination
+ # domain doesn't exist. But DAViCal doesn't know about domains, so
+ # we let that slide.
+ #
+ # @param src [User] the source user to be moved.
+ #
+ # @param dst [User] the destination user being moved to.
+ #
def mv_user(src, dst)
- # Switch the given usernames. DAViCal uses foreign keys properly
- # and only supports postgres, so we let the ON UPDATE CASCADE
- # trigger handle most of the work.
-
- # It's obviously an error if the source user does not exist. It
- # would also be an error if the destination domain didn't exist;
- # however, DAViCal doesn't know about domains, so we let that slide.
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
-
- # And it's an error if the destination user exists already.
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
sql_queries = ['UPDATE usr SET username = $1 WHERE username = $2']
require 'common/dovecot_plugin'
require 'mv/mv_plugin'
+
+# Handle moving (renaming) Dovecot users on the filesystem.
+#
class DovecotMv
include DovecotPlugin
include MvPlugin
+
+ # Move the Dovecot user *src* to *dst*. This relocates the user's
+ # directory within the Dovecot mailstore (on the filesystem).
+ #
+ # This fails if the source user does not exist, or if the
+ # destination user already exists before the move.
+ #
+ # But is it an error if the target domain does not exist? That's a
+ # bit subtle... The domain may exist in the database, but if it
+ # has not received any mail yet, then its directory won't exist
+ # on-disk.
+ #
+ # There are two possible "oops" scenarios resulting from the fact
+ # that we may run either the Postfixadmin move first or the
+ # Dovecot move first. If we move the user in the database, we
+ # definitely want to move him on disk (that is, we should create
+ # the directory here). But if we move him on disk first, then we
+ # don't know if the database move will fail! We don't want to move
+ # his mail files if he won't get moved in the database.
+ #
+ # Faced with two equally-bad (but easy-to-fix) options, we do the
+ # simplest thing and fail if the destination domain directory
+ # doesn't exist. If nothing else, this is at least consistent.
+ #
+ # @param src [User] the source user to be moved.
+ #
+ # @param dst [User] the destination user being moved to.
+ #
def mv_user(src, dst)
- # It's obviously an error if the source user does not exist.
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
-
- # And it's an error if the destination user exists already.
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
- # But is it an error if the target domain does not exist? That's a
- # bit subtle... The domain may exist in the database, but if it
- # has not received any mail yet, then its directory won't exist
- # on-disk.
- #
- # There are two possible "oops" scenarios resulting from the fact
- # that we may run either the Postfixadmin move first or the
- # Dovecot move first. If we move the user in the database, we
- # definitely want to move him on disk (that is, we should create
- # the directory here). But if we move him on disk first, then we
- # don't know if the database move will fail! We don't want to move
- # his mail files if he won't get moved in the database.
- #
- # Faced with two equally-bad (but easy-to-fix) options, we do the
- # simplest thing and fail if the destination domain directory
- # doesn't exist. If nothing else, this is at least consistent.
+ # See the docstring...
if not self.domain_exists(dst.domain()) then
raise NonexistentDomainError.new(dst.domainpart())
end
require 'common/postfixadmin_plugin'
require 'mv/mv_plugin'
+
+# Handle moving (renaming) of users in the Postfixadmin database.
+#
class PostfixadminMv
include PostfixadminPlugin
include MvPlugin
+
+ # Move the user *src* to *dst* within the Postfixadmin
+ # database. This should "rename" him in _every_ table where he is
+ # referenced. Unfortunately that must be done manually.
+ #
+ # This can fail is *src* does not exist, or if *dst* already exists
+ # before the move. It will also fail if the domain associated with
+ # the user *dst* does not exist.
+ #
+ # @param src [User] the source user to be moved.
+ #
+ # @param dst [User] the destination user being moved to.
+ #
def mv_user(src, dst)
- # It's obviously an error if the source user does not exist.
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
- # And if the destination domain does not exist.
if not domain_exists(dst.domain())
raise NonexistentDomainError.new(dst.domain.to_s())
end
- # But the destination *user* should *not* exist.
- # And it's an error if the destination user exists already.
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
mailbox_query = 'UPDATE mailbox SET '
require 'common/roundcube_plugin'
require 'mv/mv_plugin'
+# Handle moving (renaming) of users in the Roundcube
+# database. Roundcube has no concept of domains.
+#
class RoundcubeMv
include RoundcubePlugin
include MvPlugin
+ # Move the user *src* to *dst* within the Roundcube database. This
+ # should "rename" him in _every_ table where he is referenced.
+ # Roundcube uses foreign keys properly, so we let the ON UPDATE
+ # CASCADE trigger handle most of the work.
+ #
+ # This can fail is *src* does not exist, or if *dst* already exists
+ # before the move. It should also be an error if the destination
+ # domain doesn't exist. But Roundcube doesn't know about domains, so
+ # we let that slide.
+ #
+ # @param src [User] the source user to be moved.
+ #
+ # @param dst [User] the destination user being moved to.
+ #
def mv_user(src, dst)
- # It's obviously an error if the source user does not exist. It
- # would also be an error if the destination domain didn't exist;
- # however, Roundcube doesn't know about domains, so we let that slide.
raise NonexistentUserError.new(src.to_s()) if not user_exists(src)
-
- # And it's an error if the destination user exists already.
raise UserAlreadyExistsError.new(dst.to_s()) if user_exists(dst)
sql_queries = ['UPDATE users SET username = $1 WHERE username = $2;']
require 'prune/prune_plugin'
require 'rm/plugins/agendav'
+# Handle the pruning of Agendav users from its database. This class
+# doesn't need to do anything; by inheriting from {AgendavRm}, we get
+# its {AgendavRm#remove_user} method and that's all we need to prune.
+#
class AgendavPrune < AgendavRm
+ # Needed for the magic includers <tt>run()</tt> method.
include PrunePlugin
end
require 'prune/prune_plugin'
require 'rm/plugins/davical'
+# Handle the pruning of DAViCal users from its database. This class
+# doesn't need to do anything; by inheriting from {DavicalRm}, we get
+# its {DavicalRm#remove_user} method and that's all we need to prune.
+#
class DavicalPrune < DavicalRm
+ # Needed for the magic includers <tt>run()</tt> method.
include PrunePlugin
end
require 'prune/prune_plugin'
require 'rm/plugins/dovecot'
+# Handle the pruning of Dovecot users from its database. This class
+# doesn't need to do anything; by inheriting from {DovecotRm}, we get
+# its {DovecotRm#remove_user} method and that's all we need to prune.
+#
class DovecotPrune < DovecotRm
+ # Needed for the magic includers <tt>run()</tt> method.
include PrunePlugin
end
require 'prune/prune_plugin'
require 'rm/plugins/postfixadmin'
-class PostfixadminPrune < PostfixadminRm
- # We don't need the ability to remove "left over" users or
- # domains, since "left over" is with respect to what's in
- # PostfixAdmin. In other words, the other plugins check themselves
- # against PostfixAdmin, it doesn't make sense to check PostfixAdmin
- # against itself.
-end
+# This class does absolutely nothing except allow us to blindly
+# instantiate classes based on the mode name and configured plugins.
+#
+# We don't need the ability to remove "left over" Postfixadmin users
+# or domains, since "left over" is with respect to what's in
+# Postfixadmin itself. The other pruning plugins check themselves
+# against Postfixadmin -- it doesn't make sense to check Postfixadmin
+# against itself.
+#
+class PostfixadminPrune < PostfixadminRm; end
require 'prune/prune_plugin'
require 'rm/plugins/roundcube'
+
+# Handle the pruning of Roundcube users from its database. This class
+# doesn't need to do anything; by inheriting from {RoundcubeRm}, we get
+# its {RoundcubeRm#remove_user} method and that's all we need to prune.
+#
class RoundcubePrune < RoundcubeRm
+ # Needed for the magic includers <tt>run()</tt> method.
include PrunePlugin
end
require 'prune/plugins/postfixadmin'
require 'rm/rm_dummy_runner'
+# Dummy implementation of a {PruneRunner}. Its <tt>run()</tt> method will
+# tell you what would have been pruned, but will not actually perform
+# the operation.
+#
class PruneDummyRunner
include Runner
+
+ # Pretend to prune unused domains and users. Some "what if"
+ # information will be output to stdout.
+ #
+ # The prune mode is the main application of the "dummy" runners,
+ # since it performs some computation outside of the plugins
+ # themselves. This lets the user know which users and domains would
+ # be removed and can help prevent mistakes or even find bugs in the
+ # prune code, if it looks like something will be removed that
+ # shouldn't be!
+ #
+ # @param cfg [Configuration] the configuration options to pass to
+ # the *plugin* we're runnning.
+ #
+ # @param plugin [Class] plugin class that will do the pruning.
+ #
def run(cfg, plugin)
# We don't want to check the PostfixAdmin database against itself.
return if plugin.class == PostfixadminPrune
-require 'rm/rm_plugin'
+require 'common/plugin.rb'
+# Plugins for pruning users. By "pruning," we mean the removal of
+# leftover non-PostfixAdmin users after the associated user has been
+# removed from the Postfixadmin database.
+#
module PrunePlugin
- #
- # Plugins for the removal of leftover non-PostfixAdmin users,
- # i.e. after an user has been removed from the PostfixAdmin
- # database.
- #
+
+ # Absorb the subclass run() magic from the Plugin::Run module.
extend Plugin::Run
+ # The runner class associated with pruning plugins.
+ #
+ # @return [Class] the {PruneRunner} class.
+ #
def self.runner()
return PruneRunner
end
+
+ # The "dummy" runner class associated with pruning plugins.
+ #
+ # @return [Class] the {PruneDummyRunner} class.
+ #
def self.dummy_runner
return PruneDummyRunner
end
- def get_leftover_domains(db_domains)
- # Given a list of domains, determine which domains belonging to
- # this plugin are not contained in the given list.
+ # Determine which domains are "left over" for this plugin. A domain
+ # is considered "left over" if it has been removed from Postfixadmin
+ # but not some other plugin.
+ #
+ # The leftovers are determined with respect to the list *db_domains*
+ # of domains that Postfixadmin knows about.
+ #
+ # @param db_domains [Array<Domain>] a list of domains that are present
+ # in the Postfixadmin database.
+ #
+ # @return [Array<Domain>] a list of domains known to this plugin but
+ # not to Postfixadmin.
+ #
+ def get_leftover_domains(db_domains)
# WARNING! Array difference doesn't work for some reason.
return list_domains().select{ |d| !db_domains.include?(d) }
end
- def get_leftover_users(db_users)
- # Given a list of users, determine which users belonging to
- # this plugin are not contained in the given list.
+ # Determine which users are "left over" for this plugin. A user
+ # is considered "left over" if it has been removed from Postfixadmin
+ # but not some other plugin.
+ #
+ # The leftovers are determined with respect to the list *db_users*
+ # of users that Postfixadmin knows about.
+ #
+ # @param db_users [Array<User>] a list of users that are present
+ # in the Postfixadmin database.
+ #
+ # @return [Array<User>] a list of users known to this plugin but
+ # not to Postfixadmin.
+ #
+ def get_leftover_users(db_users)
# WARNING! Array difference doesn't work for some reason.
return list_users().select{ |u| !db_users.include?(u) }
end
require 'prune/plugins/postfixadmin'
require 'rm/rm_runner'
+# Perform the pruning of users/domains using {PrunePlugin}s.
+#
class PruneRunner
include Runner
+ # Run *plugin* to prune leftover users and directories.
+ #
+ # @param cfg [Configuration] configuration options passed to
+ # {PostfixadminPrune}.
+ #
+ # @param plugin [Class] plugin class that will perform the pruning.
+ #
def run(cfg, plugin)
# We don't want to check the PostfixAdmin database against itself.
return if plugin.class == PostfixadminPrune
leftovers = plugin.get_leftover_users(db_users)
leftovers += plugin.get_leftover_domains(db_domains)
+ # We're counting on our PrunePlugin also being an RmPlugin here.
rm_runner = RmRunner.new()
rm_runner.run(cfg, plugin, *leftovers)
end
require 'common/agendav_plugin'
require 'rm/rm_plugin'
+
+# Handle the removal of Agendav users from its database. Agendav has
+# no concept of domains.
+#
class AgendavRm
include AgendavPlugin
include RmPlugin
- def delete_user(user)
- # Delete the given username and any records in other tables
- # belonging to it.
+ # Remove *user* from the Agendav database. This should remove him
+ # from _every_ table in which he is referenced.
+ #
+ # @param user [User] the user to remove.
+ #
+ def remove_user(user)
raise NonexistentUserError.new(user.to_s()) if not user_exists(user)
sql_queries = ['DELETE FROM prefs WHERE username = $1;']
require 'common/davical_plugin'
require 'rm/rm_plugin'
+# Handle the removal of DAViCal users from its database. DAViCal has
+# no concept of domains.
+#
class DavicalRm
- #
- # DAViCal only supports Postgres, so even if we ever are
- # database-agnostic, this plugin can't be.
- #
+
include DavicalPlugin
include RmPlugin
- def delete_user(user)
- # Delete the given username. DAViCal uses foreign keys properly
- # and only supports postgres, so we let the ON DELETE CASCADE
- # trigger handle most of the work.
+ # Remove *user* from the DAViCal database. This should remove him
+ # from _every_ table in which he is referenced. Fortunately, DAViCal
+ # uses foreign keys properly (and only supports postgres, where they
+ # work!), so we can let the ON DELETE CASCADE trigger handle most of
+ # the work.
+ #
+ # @param user [User] the user to remove.
+ #
+ def remove_user(user)
raise NonexistentUserError.new(user.to_s()) if not user_exists(user)
sql_queries = ['DELETE FROM usr WHERE username = $1']
-# Needed for rm_r.
require 'fileutils'
require 'common/dovecot_plugin'
require 'rm/rm_plugin'
+
+# Handle the removal of users and domains from the Dovecot mailstore
+# (the filesystem).
+#
class DovecotRm
include DovecotPlugin
include RmPlugin
- def delete_domain(domain)
+ # Remove *domain* from the Dovecot mailstore. This just runs "rm -r"
+ # on the domain directory if it exists.
+ #
+ # @param domain [Domain] the domain to remove.
+ #
+ def remove_domain(domain)
domain_path = self.get_domain_path(domain)
if not File.directory?(domain_path)
FileUtils.rm_r(domain_path)
end
- def delete_user(user)
+
+ # Remove *user* from the Dovecot mailstore. This just runs "rm -r"
+ # on the *user*'s mailbox directory, if it exists.
+ #
+ # @param user [User] the user whose mailbox directory we want to
+ # remove.
+ #
+ def remove_user(user)
user_path = self.get_user_path(user)
if not File.directory?(user_path)
require 'common/postfixadmin_plugin'
require 'rm/rm_plugin'
+
+# Handle the removal of users and domains from the Postfixadmin database.
+#
class PostfixadminRm
include PostfixadminPlugin
include RmPlugin
- def delete_user(user)
+ # Remove *user* from the Postfixadmin database. This should remove
+ # him from _every_ table in which he is referenced. Unfortunately,
+ # Postfixadmin does not use foreign keys or ON DELETE CASCADE
+ # triggers so we need to delete the associated child table records
+ # ourselves.
+ #
+ # @param user [User] the user to remove.
+ #
+ def remove_user(user)
raise NonexistentUserError.new(user.to_s()) if not user_exists(user)
+ # Remove aliases FROM our user to some other address.
sql_queries = ['DELETE FROM alias WHERE address = $1;']
- # Wipe out any aliases pointed at our user.
+
+ # Also delete aliases that point SOLELY TO our user.
+ sql_queries << "DELETE FROM alias WHERE goto = $1;"
+
+ # But aliases don't need to point to a single user! If our user
+ # was part of a multi-recipient alias, we want to remove our user
+ # from the alias and leave the other recipients. If you're
+ # wondering about the leftover double-commas, look towards the end
+ # of the function.
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;'
end
- def delete_domain(domain)
+ # Remove *domain* from the Postfixadmin database. This should remove
+ # the domain from _every_ table in which it is referenced. It should
+ # also remove every user that belongs to the doomed domain
+ # Postfixadmin has some experimental support for triggers, but they
+ # don't do a very good job of cleaning up. Therefore we remove all
+ # users in the domain manually before removing the domain itself.
+ #
+ # Log entries (from the "log" table) are not removed since they may
+ # still contain valuable information (although they won't mention
+ # this removal).
+ #
+ # @param domain [Domain] the domain to remove.
+ #
+ def remove_domain(domain)
raise NonexistentDomainError.new(domain.to_s()) if not domain_exists(domain)
+ # First remove all users belonging to the domain. This will handle
+ # alias updates and all the sensitive crap we need to do when
+ # removing a user.
+ users = list_domains_users([domain])
+ users.each { |u| remove_user(u) }
+
+ # The domain_admins table contains one record per domain
+ # (repeating the user as necessary), so this really is sufficient.
sql_queries = ['DELETE FROM domain_admins WHERE domain = $1;']
+
+ # Some of the following queries should be redundant now that we've
+ # removed all users in the domain.
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;'
sql_queries << 'DELETE FROM domain WHERE domain = $1;'
connection.close()
end
-
- protected;
-
- def domain_exists(domain)
- count = 0
-
- connection = PGconn.connect(@db_host, @db_port, @db_opts, @db_tty,
- @db_name, @db_user, @db_pass)
-
- sql_query = 'SELECT COUNT(domain) as count FROM domain WHERE domain = $1;'
- connection.query(sql_query, [domain.to_s()]) do |result|
- return false if result.ntuples() < 1
- begin
- count = result.getvalue(0,0).to_i()
- return false if count.nil?
- rescue StandardError
- return false
- end
- end
-
- connection.close()
-
- return (count > 0)
- end
-
end
require 'common/roundcube_plugin'
require 'rm/rm_plugin'
+# Handle removal of Roundcube users from its database. Roundcube has
+# no concept of domains.
+#
class RoundcubeRm
include RoundcubePlugin
include RmPlugin
- def delete_user(user)
- # Delete the given username and any records in other tables
- # belonging to it.
+ # Remove *user* from the Roundcube database. This should remove him
+ # from _every_ table in which he is referenced. Fortunately the
+ # Roundcube developers were nice enough to include DBMS-specific
+ # install and upgrade scripts, so Postgres can take advantage of ON
+ # DELETE triggers.
+ #
+ # @param user [User] the user to remove.
+ #
+ def remove_user(user)
raise NonexistentUserError.new(user.to_s()) if not user_exists(user)
+ # Get the primary key for this user in the "users" table.
user_id = self.get_user_id(user)
- # The Roundcube developers were nice enough to include
- # DBMS-specific install and upgrade scripts, so Postgres can take
- # advantage of ON DELETE triggers. Here's an example:
- #
- # ...
- # user_id integer NOT NULL
- # REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE
- #
- # This query is of course necessary with any DBMS:
+ # Thanks to the ON DELETE triggers, this will remove all child
+ # records associated with user_id too.
sql_queries = ['DELETE FROM users WHERE user_id = $1::int;']
connection = PGconn.connect(@db_host, @db_port, @db_opts, @db_tty,
require 'common/runner'
+# Dummy implementation of a {RmRunner}. Its <tt>run()</tt> method will
+# tell you what would have been removed, but will not actually perform
+# the operation.
+#
class RmDummyRunner
include Runner
+
+ # Pretend to remove *targets*. Some "what if"
+ # information will be output to stdout.
+ #
+ # This dummy runner is not particularly useful on its own. About the
+ # only thing it does is let you know that the users/domains in
+ # *targets* do in fact exist (through their descriptions). It's used
+ # to good effect by {PruneDummyRunner}, though.
+ #
+ # @param cfg [Configuration] the configuration options to pass to
+ # the *plugin* we're runnning.
+ #
+ # @param plugin [RmPlugin] plugin that will perform the move.
+ #
+ # @param targets [Array<User,Domain>] the users and domains to be
+ # removed.
+ #
def run(cfg, plugin, *targets)
targets.each do |target|
target_description = plugin.describe(target)
require 'common/plugin.rb'
+#
+# Plugins for the removal of users and domains.
+#
module RmPlugin
- #
- # Plugins for the removal of users.
- #
+ # Absorb the subclass run() magic from the Plugin::Run module.
extend Plugin::Run
+ # The runner class associated with removal plugins.
+ #
+ # @return [Class] the {RmRunner} class.
+ #
def self.runner()
return RmRunner
end
+
+ # The "dummy" runner class associated with removal plugins.
+ #
+ # @return [Class] the {RmDummyRunner} class.
+ #
def self.dummy_runner()
return RmDummyRunner
end
- def delete(target)
- # A generic version of delete_user/delete_domain that
+
+ # Remove the *target* domain or user. This is a generic version of
+ # the {#remove_user} and {#remove_domain} operations that will
+ # dispatch based on the type of *target*.
+ #
+ # @param target [User,Domain] the user or domain to remove.
+ #
+ def remove(target)
+ # A generic version of remove_user/remove_domain that
# dispatches base on the class of the target.
if target.is_a?(User)
- return delete_user(target)
+ return remove_user(target)
elsif target.is_a?(Domain)
- return delete_domain(target)
+ return remove_domain(target)
else
raise NotImplementedError
end
end
- def delete_domain(domain)
- # Delete the given domain. Some plugins don't have a concept of
- # domains, so just delete all users with a username that looks
- # like it's in the given domain.
+
+ # Remove the given *domain*. Some plugins don't have a concept of
+ # domains, so the default implementation here removes all users that
+ # look like they belong to *domain*. Subclasses can be smarter.
+ #
+ # @param domain [Domain] the domain to remove.
+ #
+ def remove_domain(domain)
users = list_domains_users([domain])
+ # It's possible for a domain to exist with no users, but this
+ # default implementation is based on the assumption that it should
+ # work for plugins having no "domain" concept.
raise NonexistentDomainError.new(domain.to_s()) if users.empty?
users.each do |u|
- delete_user(u)
+ remove_user(u)
end
end
- def delete_user(user)
- # Delete the given user.
+
+ # The interface for the "remove a user" operation. Subclasses
+ # need to implement this method so that it removes *user*.
+ #
+ # @param user [User] the user to remove.
+ #
+ def remove_user(user)
raise NotImplementedError
end
require 'common/errors'
require 'common/runner'
+# Perform the removal of users/domains using {RmPlugin}s.
+#
class RmRunner
include Runner
+ # Run *plugin* to remove the users/domains in *targets*. The method
+ # signature includes the unused *cfg* for consistency with the
+ # runners that do need a {Configuration}.
+ #
+ # @param cfg [Configuration] unused.
+ #
+ # @param plugin [Class] plugin class that will perform the removal.
+ #
+ # @param targets [Array<User,Domain>] the users and domains to be
+ # removed.
+ #
def run(cfg, plugin, *targets)
targets.each do |target|
remove_target(plugin, target)
end
end
- private;
+ protected;
+
+ # Remove *target* using *plugin*. This operation is separate from
+ # the <tt>run()</tt> method so that it can be accessed by the prune
+ # runner.
+ #
+ # @param plugin [RmPlugin] the plugin that will remove the *target*.
+ #
+ # @param target [User,Domain] the user or domain to remove.
+ #
def remove_target(plugin, target)
- # Wrap the "remove this thing" operation so that it can be reused
- # in the prine plugin.
target_description = plugin.describe(target)
begin
- plugin.delete(target)
+ plugin.remove(target)
msg = "Removed #{target.class.to_s().downcase()} #{target}"
# Only append the extra description if it's useful.