diff -r b5cc967a45ad -r 33d15936b53a VirtualMailManager/cli/subcommands.py --- a/VirtualMailManager/cli/subcommands.py Fri Dec 21 12:28:56 2012 +0000 +++ b/VirtualMailManager/cli/subcommands.py Sun Dec 30 16:10:29 2012 +0000 @@ -9,150 +9,83 @@ """ import locale -import os +import platform +from argparse import Action, ArgumentParser, ArgumentTypeError, \ + RawDescriptionHelpFormatter from textwrap import TextWrapper from time import strftime, strptime from VirtualMailManager import ENCODING -from VirtualMailManager.cli import get_winsize, prog, w_err, w_std -from VirtualMailManager.cli.clihelp import help_msgs +from VirtualMailManager.cli import get_winsize, w_err, w_std from VirtualMailManager.common import human_size, size_in_bytes, \ version_str, format_domain_default from VirtualMailManager.constants import __copyright__, __date__, \ __version__, ACCOUNT_EXISTS, ALIAS_EXISTS, ALIASDOMAIN_ISDOMAIN, \ - DOMAIN_ALIAS_EXISTS, INVALID_ARGUMENT, EX_MISSING_ARGS, \ - RELOCATED_EXISTS, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED + DOMAIN_ALIAS_EXISTS, INVALID_ARGUMENT, RELOCATED_EXISTS, TYPE_ACCOUNT, \ + TYPE_ALIAS, TYPE_RELOCATED from VirtualMailManager.errors import VMMError from VirtualMailManager.password import list_schemes from VirtualMailManager.serviceset import SERVICES __all__ = ( - 'Command', 'RunContext', 'cmd_map', 'usage', 'alias_add', 'alias_delete', - 'alias_info', 'aliasdomain_add', 'aliasdomain_delete', 'aliasdomain_info', - 'aliasdomain_switch', 'catchall_add', 'catchall_info', 'catchall_delete', - 'config_get', 'config_set', 'configure', - 'domain_add', 'domain_delete', 'domain_info', 'domain_quota', - 'domain_services', 'domain_transport', 'domain_note', 'get_user', 'help_', - 'list_domains', 'list_pwschemes', 'list_users', 'list_aliases', - 'list_relocated', 'list_addresses', 'relocated_add', 'relocated_delete', - 'relocated_info', 'user_add', 'user_delete', 'user_info', 'user_name', - 'user_password', 'user_quota', 'user_services', 'user_transport', - 'user_note', 'version', + 'RunContext', 'alias_add', 'alias_delete', 'alias_info', 'aliasdomain_add', + 'aliasdomain_delete', 'aliasdomain_info', 'aliasdomain_switch', + 'catchall_add', 'catchall_delete', 'catchall_info', 'config_get', + 'config_set', 'configure', 'domain_add', 'domain_delete', 'domain_info', + 'domain_note', 'domain_quota', 'domain_services', 'domain_transport', + 'get_user', 'list_addresses', 'list_aliases', 'list_domains', + 'list_pwschemes', 'list_relocated', 'list_users', 'relocated_add', + 'relocated_delete', 'relocated_info', 'setup_parser', 'user_add', + 'user_delete', 'user_info', 'user_name', 'user_note', 'user_password', + 'user_quota', 'user_services', 'user_transport', ) +WS_ROWS = get_winsize()[1] - 2 + _ = lambda msg: msg -txt_wrpr = TextWrapper(width=get_winsize()[1] - 1) -cmd_map = {} - - -class Command(object): - """Container class for command information.""" - __slots__ = ('name', 'alias', 'func', 'args', 'descr') - FMT_HLP_USAGE = """ -usage: %(prog)s %(name)s %(args)s - %(prog)s %(alias)s %(args)s -""" - - def __init__(self, name, alias, func, args, descr): - """Create a new Command instance. - - Arguments: - - `name` : str - the command name, e.g. ``addalias`` - `alias` : str - the command's short alias, e.g. ``aa`` - `func` : callable - the function to handle the command - `args` : str - argument placeholders, e.g. ``aliasaddress`` - `descr` : str - short description of the command - """ - self.name = name - self.alias = alias - self.func = func - self.args = args - self.descr = descr - - @property - def usage(self): - """the command's usage info.""" - return '%s %s %s' % (prog, self.name, self.args) - - def help_(self): - """Print the Command's help message to stdout.""" - old_ii = txt_wrpr.initial_indent - old_si = txt_wrpr.subsequent_indent - - txt_wrpr.subsequent_indent = (len(self.name) + 2) * ' ' - w_std(txt_wrpr.fill('%s: %s' % (self.name, self.descr))) - - info = Command.FMT_HLP_USAGE % dict(alias=self.alias, args=self.args, - name=self.name, prog=prog) - w_std(info) - - txt_wrpr.initial_indent = txt_wrpr.subsequent_indent = ' ' - try: - [w_std(txt_wrpr.fill(_(para)) + '\n') for para - in help_msgs[self.name]] - except KeyError: - w_err(1, _("Subcommand '%s' is not yet documented." % self.name), - 'see also: vmm(1)') +txt_wrpr = TextWrapper(width=WS_ROWS) class RunContext(object): """Contains all information necessary to run a subcommand.""" - __slots__ = ('argc', 'args', 'cget', 'hdlr', 'scmd') + __slots__ = ('args', 'cget', 'hdlr') plan_a_b = _('Plan A failed ... trying Plan B: %(subcommand)s %(object)s') - def __init__(self, argv, handler, command): + def __init__(self, args, handler): """Create a new RunContext""" - self.argc = len(argv) - self.args = argv[:] # will be moved to argparse + self.args = args self.cget = handler.cfg_dget self.hdlr = handler - self.scmd = command def alias_add(ctx): """create a new alias e-mail address""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing alias address and destination.'), - ctx.scmd) - elif ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing destination address.'), ctx.scmd) - ctx.hdlr.alias_add(ctx.args[2].lower(), *ctx.args[3:]) + ctx.hdlr.alias_add(ctx.args.address.lower(), *ctx.args.destination) def alias_delete(ctx): """delete the specified alias e-mail address or one of its destinations""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing alias address.'), ctx.scmd) - elif ctx.argc < 4: - ctx.hdlr.alias_delete(ctx.args[2].lower()) - else: - ctx.hdlr.alias_delete(ctx.args[2].lower(), ctx.args[3:]) + destination = ctx.args.destination if ctx.args.destination else None + ctx.hdlr.alias_delete(ctx.args.address.lower(), destination) def alias_info(ctx): """show the destination(s) of the specified alias""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing alias address.'), ctx.scmd) - address = ctx.args[2].lower() + address = ctx.args.address.lower() try: _print_aliase_info(address, ctx.hdlr.alias_info(address)) except VMMError as err: if err.code is ACCOUNT_EXISTS: w_err(0, ctx.plan_a_b % {'subcommand': 'userinfo', 'object': address}) - ctx.scmd = ctx.args[1] = 'userinfo' + ctx.args.scmd = 'userinfo' + ctx.args.details = None user_info(ctx) elif err.code is RELOCATED_EXISTS: w_err(0, ctx.plan_a_b % {'subcommand': 'relocatedinfo', 'object': address}) - ctx.scmd = ctx.args[1] = 'relocatedinfo' + ctx.args.scmd = 'relocatedinfo' relocated_info(ctx) else: raise @@ -160,33 +93,25 @@ def aliasdomain_add(ctx): """create a new alias for an existing domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing alias domain name and destination ' - 'domain name.'), ctx.scmd) - elif ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing destination domain name.'), - ctx.scmd) - ctx.hdlr.aliasdomain_add(ctx.args[2].lower(), ctx.args[3].lower()) + ctx.hdlr.aliasdomain_add(ctx.args.fqdn.lower(), + ctx.args.destination.lower()) def aliasdomain_delete(ctx): """delete the specified alias domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing alias domain name.'), ctx.scmd) - ctx.hdlr.aliasdomain_delete(ctx.args[2].lower()) + ctx.hdlr.aliasdomain_delete(ctx.args.fqdn.lower()) def aliasdomain_info(ctx): """show the destination of the given alias domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing alias domain name.'), ctx.scmd) + fqdn = ctx.args.fqdn.lower() try: - _print_aliasdomain_info(ctx.hdlr.aliasdomain_info(ctx.args[2].lower())) + _print_aliasdomain_info(ctx.hdlr.aliasdomain_info(fqdn)) except VMMError as err: if err.code is ALIASDOMAIN_ISDOMAIN: w_err(0, ctx.plan_a_b % {'subcommand': 'domaininfo', - 'object': ctx.args[2].lower()}) - ctx.scmd = ctx.args[1] = 'domaininfo' + 'object': fqdn}) + ctx.args.scmd = 'domaininfo' domain_info(ctx) else: raise @@ -194,125 +119,79 @@ def aliasdomain_switch(ctx): """assign the given alias domain to an other domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing alias domain name and destination ' - 'domain name.'), ctx.scmd) - elif ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing destination domain name.'), - ctx.scmd) - ctx.hdlr.aliasdomain_switch(ctx.args[2].lower(), ctx.args[3].lower()) + ctx.hdlr.aliasdomain_switch(ctx.args.fqdn.lower(), + ctx.args.destination.lower()) def catchall_add(ctx): """create a new catchall alias e-mail address""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain and destination.'), - ctx.scmd) - elif ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing destination address.'), ctx.scmd) - ctx.hdlr.catchall_add(ctx.args[2].lower(), *ctx.args[3:]) + ctx.hdlr.catchall_add(ctx.args.fqdn.lower(), *ctx.args.destination) def catchall_delete(ctx): """delete the specified destination or all of the catchall destination""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) - elif ctx.argc < 4: - ctx.hdlr.catchall_delete(ctx.args[2].lower()) - else: - ctx.hdlr.catchall_delete(ctx.args[2].lower(), ctx.args[3:]) + destination = ctx.args.destination if ctx.args.destination else None + ctx.hdlr.catchall_delete(ctx.args.fqdn.lower(), destination) def catchall_info(ctx): """show the catchall destination(s) of the specified domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) - address = ctx.args[2].lower() + address = ctx.args.fqdn.lower() _print_catchall_info(address, ctx.hdlr.catchall_info(address)) def config_get(ctx): """show the actual value of the configuration option""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _("Missing option name."), ctx.scmd) - noop = lambda option: option opt_formater = { 'misc.dovecot_version': version_str, 'domain.quota_bytes': human_size, } - option = ctx.args[2].lower() + option = ctx.args.option.lower() w_std('%s = %s' % (option, opt_formater.get(option, noop)(ctx.cget(option)))) def config_set(ctx): """set a new value for the configuration option""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing option and new value.'), ctx.scmd) - if ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing new configuration value.'), - ctx.scmd) - ctx.hdlr.cfg_set(ctx.args[2].lower(), ctx.args[3]) + ctx.hdlr.cfg_set(ctx.args.option.lower(), ctx.args.value) def configure(ctx): """start interactive configuration mode""" - if ctx.argc < 3: - ctx.hdlr.configure() - else: - ctx.hdlr.configure(ctx.args[2].lower()) + ctx.hdlr.configure(ctx.args.section) def domain_add(ctx): """create a new domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) - elif ctx.argc < 4: - ctx.hdlr.domain_add(ctx.args[2].lower()) - else: - ctx.hdlr.domain_add(ctx.args[2].lower(), ctx.args[3]) + fqdn = ctx.args.fqdn.lower() + transport = ctx.args.transport.lower if ctx.args.transport else None + ctx.hdlr.domain_add(fqdn, transport) if ctx.cget('domain.auto_postmaster'): - w_std(_('Creating account for postmaster@%s') % ctx.args[2].lower()) - ctx.scmd = 'useradd' - ctx.args = [prog, ctx.scmd, 'postmaster@' + ctx.args[2].lower()] - ctx.argc = 3 + w_std(_('Creating account for postmaster@%s') % fqdn) + ctx.args.scmd = 'useradd' + ctx.args.address = 'postmaster@%s' % fqdn + ctx.args.password = None user_add(ctx) def domain_delete(ctx): """delete the given domain and all its alias domains""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) - elif ctx.argc < 4: - ctx.hdlr.domain_delete(ctx.args[2].lower()) - elif ctx.args[3].lower() == 'force': - ctx.hdlr.domain_delete(ctx.args[2].lower(), True) - else: - usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % ctx.args[3], - ctx.scmd) + ctx.hdlr.domain_delete(ctx.args.fqdn.lower(), ctx.args.force) def domain_info(ctx): """display information about the given domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) - if ctx.argc < 4: - details = None - else: - details = ctx.args[3].lower() - if details not in ('accounts', 'aliasdomains', 'aliases', 'full', - 'relocated', 'catchall'): - usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % details, - ctx.scmd) + fqdn = ctx.args.fqdn.lower() + details = ctx.args.details try: - info = ctx.hdlr.domain_info(ctx.args[2].lower(), details) + info = ctx.hdlr.domain_info(fqdn, details) except VMMError as err: if err.code is DOMAIN_ALIAS_EXISTS: w_err(0, ctx.plan_a_b % {'subcommand': 'aliasdomaininfo', - 'object': ctx.args[2].lower()}) - ctx.scmd = ctx.args[1] = 'aliasdomaininfo' + 'object': fqdn}) + ctx.args.scmd = 'aliasdomaininfo' aliasdomain_info(ctx) else: raise @@ -349,151 +228,40 @@ def domain_quota(ctx): """update the quota limit of the specified domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain name and storage value.'), - ctx.scmd) - if ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing storage value.'), ctx.scmd) - messages = 0 - force = None - try: - bytes_ = size_in_bytes(ctx.args[3]) - except (ValueError, TypeError): - usage(INVALID_ARGUMENT, _("Invalid storage value: '%s'") % - ctx.args[3], ctx.scmd) - if ctx.argc < 5: - pass - elif ctx.argc < 6: - try: - messages = int(ctx.args[4]) - except ValueError: - if ctx.args[4].lower() != 'force': - usage(INVALID_ARGUMENT, - _("Neither a valid number of messages nor the keyword " - "'force': '%s'") % ctx.args[4], ctx.scmd) - force = 'force' - else: - try: - messages = int(ctx.args[4]) - except ValueError: - usage(INVALID_ARGUMENT, - _("Not a valid number of messages: '%s'") % ctx.args[4], - ctx.scmd) - if ctx.args[5].lower() != 'force': - usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % ctx.args[5], - ctx.scmd) - force = 'force' - ctx.hdlr.domain_quotalimit(ctx.args[2].lower(), bytes_, messages, force) + force = 'force' if ctx.args.force else None + ctx.hdlr.domain_quotalimit(ctx.args.fqdn.lower(), ctx.args.storage, + ctx.args.messages, force) def domain_services(ctx): """allow all named service and block the uncredited.""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd) - services = [] - force = False - if ctx.argc is 3: - pass - elif ctx.argc is 4: - arg = ctx.args[3].lower() - if arg in SERVICES: - services.append(arg) - elif arg == 'force': - force = True - else: - usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % arg, - ctx.scmd) - else: - services.extend([service.lower() for service in ctx.args[3:-1]]) - arg = ctx.args[-1].lower() - if arg == 'force': - force = True - else: - services.append(arg) - unknown = [service for service in services if service not in SERVICES] - if unknown: - usage(INVALID_ARGUMENT, _('Invalid service arguments: %s') % - ' '.join(unknown), ctx.scmd) - ctx.hdlr.domain_services(ctx.args[2].lower(), (None, 'force')[force], - *services) + force = 'force' if ctx.args.force else None + services = ctx.args.services if ctx.args.services else [] + ctx.hdlr.domain_services(ctx.args.fqdn.lower(), force, *services) def domain_transport(ctx): """update the transport of the specified domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain name and new transport.'), - ctx.scmd) - if ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing new transport.'), ctx.scmd) - if ctx.argc < 5: - ctx.hdlr.domain_transport(ctx.args[2].lower(), ctx.args[3]) - else: - force = ctx.args[4].lower() - if force != 'force': - usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % force, - ctx.scmd) - ctx.hdlr.domain_transport(ctx.args[2].lower(), ctx.args[3], force) + force = 'force' if ctx.args.force else None + ctx.hdlr.domain_transport(ctx.args.fqdn.lower(), + ctx.args.transport.lower(), force) def domain_note(ctx): """update the note of the given domain""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing domain name.'), - ctx.scmd) - elif ctx.argc < 4: - note = None - else: - note = ' '.join(ctx.args[3:]) - ctx.hdlr.domain_note(ctx.args[2].lower(), note) + ctx.hdlr.domain_note(ctx.args.fqdn.lower(), ctx.args.note) def get_user(ctx): """get the address of the user with the given UID""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing UID.'), ctx.scmd) - _print_info(ctx, ctx.hdlr.user_by_uid(ctx.args[2]), _('Account')) - - -def help_(ctx): - """print help messages.""" - if ctx.argc > 2: - hlptpc = ctx.args[2].lower() - if hlptpc in cmd_map: - topic = hlptpc - else: - for scmd in cmd_map.values(): - if scmd.alias == hlptpc: - topic = scmd.name - break - else: - usage(INVALID_ARGUMENT, _("Unknown help topic: '%s'") % - ctx.args[2], ctx.scmd) - if topic != 'help': - return cmd_map[topic].help_() - - old_ii = txt_wrpr.initial_indent - old_si = txt_wrpr.subsequent_indent - txt_wrpr.initial_indent = ' ' - # len(max(_overview.iterkeys(), key=len)) #Py25 - txt_wrpr.subsequent_indent = 20 * ' ' - order = sorted(list(cmd_map.keys())) - - w_std(_('List of available subcommands:') + '\n') - for key in order: - w_std('\n'.join(txt_wrpr.wrap('%-18s %s' % (key, cmd_map[key].descr)))) - - txt_wrpr.initial_indent = old_ii - txt_wrpr.subsequent_indent = old_si - txt_wrpr.initial_indent = '' + _print_info(ctx, ctx.hdlr.user_by_uid(ctx.args.uid), _('Account')) def list_domains(ctx): """list all domains / search domains by pattern""" - matching = ctx.argc > 2 - if matching: - gids, domains = ctx.hdlr.domain_list(ctx.args[2].lower()) - else: - gids, domains = ctx.hdlr.domain_list() + matching = True if ctx.args.pattern else False + pattern = ctx.args.pattern.lower() if matching else None + gids, domains = ctx.hdlr.domain_list(pattern) _print_domain_list(gids, domains, matching) @@ -519,11 +287,9 @@ combining all three.""" if limit is None: limit = TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED - matching = ctx.argc > 2 - if matching: - gids, addresses = ctx.hdlr.address_list(limit, ctx.args[2].lower()) - else: - gids, addresses = ctx.hdlr.address_list(limit) + matching = True if ctx.args.pattern else False + pattern = ctx.args.pattern.lower() if matching else None + gids, addresses = ctx.hdlr.address_list(limit, pattern) _print_address_list(limit, gids, addresses, matching) @@ -544,26 +310,17 @@ def relocated_add(ctx): """create a new record for a relocated user""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, - _('Missing relocated address and destination.'), ctx.scmd) - elif ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing destination address.'), ctx.scmd) - ctx.hdlr.relocated_add(ctx.args[2].lower(), ctx.args[3]) + ctx.hdlr.relocated_add(ctx.args.address.lower(), ctx.args.newaddress) def relocated_delete(ctx): """delete the record of the relocated user""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing relocated address.'), ctx.scmd) - ctx.hdlr.relocated_delete(ctx.args[2].lower()) + ctx.hdlr.relocated_delete(ctx.args.address.lower()) def relocated_info(ctx): """print information about a relocated user""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing relocated address.'), ctx.scmd) - relocated = ctx.args[2].lower() + relocated = ctx.args.address.lower() try: _print_relocated_info(addr=relocated, dest=ctx.hdlr.relocated_info(relocated)) @@ -571,12 +328,13 @@ if err.code is ACCOUNT_EXISTS: w_err(0, ctx.plan_a_b % {'subcommand': 'userinfo', 'object': relocated}) - ctx.scmd = ctx.args[1] = 'userinfoi' + ctx.args.scmd = 'userinfo' + ctx.args.details = None user_info(ctx) elif err.code is ALIAS_EXISTS: w_err(0, ctx.plan_a_b % {'subcommand': 'aliasinfo', 'object': relocated}) - ctx.scmd = ctx.args[1] = 'aliasinfo' + ctx.args.scmd = 'aliasinfo' alias_info(ctx) else: raise @@ -584,58 +342,36 @@ def user_add(ctx): """create a new e-mail user with the given address""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd) - elif ctx.argc < 4: - password = None - else: - password = ctx.args[3] - gen_pass = ctx.hdlr.user_add(ctx.args[2].lower(), password) - if ctx.argc < 4 and gen_pass: + gen_pass = ctx.hdlr.user_add(ctx.args.address.lower(), ctx.args.password) + if not ctx.args.password and gen_pass: w_std(_("Generated password: %s") % gen_pass) def user_delete(ctx): """delete the specified user""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd) - elif ctx.argc < 4: - ctx.hdlr.user_delete(ctx.args[2].lower()) - elif ctx.args[3].lower() == 'force': - ctx.hdlr.user_delete(ctx.args[2].lower(), True) - else: - usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % ctx.args[3], - ctx.scmd) + ctx.hdlr.user_delete(ctx.args.address.lower(), ctx.args.force) def user_info(ctx): """display information about the given address""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd) - if ctx.argc < 4: - details = None - else: - details = ctx.args[3].lower() - if details not in ('aliases', 'du', 'full'): - usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % details, - ctx.scmd) + address = ctx.args.address.lower() try: - info = ctx.hdlr.user_info(ctx.args[2].lower(), details) + info = ctx.hdlr.user_info(address, ctx.args.details) except VMMError as err: if err.code is ALIAS_EXISTS: w_err(0, ctx.plan_a_b % {'subcommand': 'aliasinfo', - 'object': ctx.args[2].lower()}) - ctx.scmd = ctx.args[1] = 'aliasinfo' + 'object': address}) + ctx.args.scmd = 'aliasinfo' alias_info(ctx) elif err.code is RELOCATED_EXISTS: w_err(0, ctx.plan_a_b % {'subcommand': 'relocatedinfo', - 'object': ctx.args[2].lower()}) - ctx.scmd = ctx.args[1] = 'relocatedinfo' + 'object': address}) + ctx.args.scmd = 'relocatedinfo' relocated_info(ctx) else: raise else: - if details in (None, 'du'): + if ctx.args.details in (None, 'du'): info['quota storage'] = _format_quota_usage(info['ql_bytes'], info['uq_bytes'], True, info['ql_domaindefault']) info['quota messages'] = \ @@ -656,265 +392,630 @@ def user_name(ctx): """set or update the real name for an address""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _("Missing e-mail address and user's name."), - ctx.scmd) - elif ctx.argc < 4: - name = None - else: - name = ctx.args[3] - ctx.hdlr.user_name(ctx.args[2].lower(), name) + ctx.hdlr.user_name(ctx.args.address.lower(), ctx.args.name) def user_password(ctx): """update the password for the given address""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd) - elif ctx.argc < 4: - password = None - else: - password = ctx.args[3] - ctx.hdlr.user_password(ctx.args[2].lower(), password) + ctx.hdlr.user_password(ctx.args.address.lower(), ctx.args.password) def user_note(ctx): """update the note of the given address""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing e-mail address.'), - ctx.scmd) - elif ctx.argc < 4: - note = None - else: - note = ' '.join(ctx.args[3:]) - ctx.hdlr.user_note(ctx.args[2].lower(), note) + ctx.hdlr.user_note(ctx.args.address.lower(), ctx.args.note) def user_quota(ctx): """update the quota limit for the given address""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing e-mail address and storage value.'), - ctx.scmd) - elif ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing storage value.'), ctx.scmd) - if ctx.args[3] != 'domain': - try: - bytes_ = size_in_bytes(ctx.args[3]) - except (ValueError, TypeError): - usage(INVALID_ARGUMENT, _("Invalid storage value: '%s'") % - ctx.args[3], ctx.scmd) - else: - bytes_ = ctx.args[3] - if ctx.argc < 5: - messages = 0 - else: - try: - messages = int(ctx.args[4]) - except ValueError: - usage(INVALID_ARGUMENT, - _("Not a valid number of messages: '%s'") % ctx.args[4], - ctx.scmd) - ctx.hdlr.user_quotalimit(ctx.args[2].lower(), bytes_, messages) + ctx.hdlr.user_quotalimit(ctx.args.address.lower(), ctx.args.storage, + ctx.args.messages) def user_services(ctx): """allow all named service and block the uncredited.""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd) - services = [] - if ctx.argc >= 4: - services.extend([service.lower() for service in ctx.args[3:]]) - unknown = [service for service in services if service not in SERVICES] - if unknown and ctx.args[3] != 'domain': - usage(INVALID_ARGUMENT, _('Invalid service arguments: %s') % - ' '.join(unknown), ctx.scmd) - ctx.hdlr.user_services(ctx.args[2].lower(), *services) + if 'domain' in ctx.args.services: + services = ['domain'] + else: + services = ctx.args.services + ctx.hdlr.user_services(ctx.args.address.lower(), *services) def user_transport(ctx): """update the transport of the given address""" - if ctx.argc < 3: - usage(EX_MISSING_ARGS, _('Missing e-mail address and transport.'), - ctx.scmd) - if ctx.argc < 4: - usage(EX_MISSING_ARGS, _('Missing transport.'), ctx.scmd) - ctx.hdlr.user_transport(ctx.args[2].lower(), ctx.args[3]) - - -def usage(errno, errmsg, subcommand=None): - """print usage message for the given command or all commands. - When errno > 0, sys,exit(errno) will interrupt the program. - """ - if subcommand and subcommand in cmd_map: - w_err(errno, _("Error: %s") % errmsg, - _("usage: ") + cmd_map[subcommand].usage) - - # TP: Please adjust translated words like the original text. - # (It's a table header.) Extract from usage text: - # usage: vmm subcommand arguments - # short long - # subcommand arguments - # - # da domainadd fqdn [transport] - # dd domaindelete fqdn [force] - u_head = _("""usage: %s subcommand arguments - short long - subcommand arguments\n""") % prog - order = sorted(list(cmd_map.keys())) - w_err(0, u_head) - for key in order: - scmd = cmd_map[key] - w_err(0, ' %-5s %-19s %s' % (scmd.alias, scmd.name, scmd.args)) - w_err(errno, '', _("Error: %s") % errmsg) - - -def version(ctx_unused): - """Write version and copyright information to stdout.""" - w_std('%s, %s %s (%s %s)\nPython %s %s %s\n\n%s\n%s %s' % (prog, - # TP: The words 'from', 'version' and 'on' are used in - # the version information, e.g.: - # vmm, version 0.5.2 (from 09/09/09) - # Python 2.5.4 on FreeBSD - _('version'), __version__, _('from'), - strftime(locale.nl_langinfo(locale.D_FMT), - strptime(__date__, '%Y-%m-%d')), - os.sys.version.split()[0], _('on'), os.uname()[0], - __copyright__, prog, - _('is free software and comes with ABSOLUTELY NO WARRANTY.'))) + ctx.hdlr.user_transport(ctx.args.address.lower(), ctx.args.transport) -def update_cmd_map(): - """Update the cmd_map, after gettext's _ was installed.""" - cmd = Command - cmd_map.update({ - # Account commands - 'getuser': cmd('getuser', 'gu', get_user, 'uid', - _('get the address of the user with the given UID')), - 'useradd': cmd('useradd', 'ua', user_add, 'address [password]', - _('create a new e-mail user with the given address')), - 'userdelete': cmd('userdelete', 'ud', user_delete, 'address [force]', - _('delete the specified user')), - 'userinfo': cmd('userinfo', 'ui', user_info, 'address [details]', - _('display information about the given address')), - 'username': cmd('username', 'un', user_name, 'address [name]', - _('set, update or delete the real name for an address')), - 'userpassword': cmd('userpassword', 'up', user_password, - 'address [password]', - _('update the password for the given address')), - 'userquota': cmd('userquota', 'uq', user_quota, - 'address storage [messages] | address domain', - _('update the quota limit for the given address')), - 'userservices': cmd('userservices', 'us', user_services, - 'address [service ...] | address domain', - _('enables the specified services and disables all ' - 'not specified services')), - 'usertransport': cmd('usertransport', 'ut', user_transport, - 'address transport | address domain', - _('update the transport of the given address')), - 'usernote': cmd('usernote', 'uo', user_note, 'address [note]', - _('set, update or delete the note of the given address')), - # Alias commands - 'aliasadd': cmd('aliasadd', 'aa', alias_add, 'address destination ...', - _('create a new alias e-mail address with one or more ' - 'destinations')), - 'aliasdelete': cmd('aliasdelete', 'ad', alias_delete, - 'address [destination ...]', - _('delete the specified alias e-mail address or one ' - 'of its destinations')), - 'aliasinfo': cmd('aliasinfo', 'ai', alias_info, 'address', - _('show the destination(s) of the specified alias')), - # AliasDomain commands - 'aliasdomainadd': cmd('aliasdomainadd', 'ada', aliasdomain_add, - 'fqdn destination', - _('create a new alias for an existing domain')), - 'aliasdomaindelete': cmd('aliasdomaindelete', 'add', aliasdomain_delete, - 'fqdn', _('delete the specified alias domain')), - 'aliasdomaininfo': cmd('aliasdomaininfo', 'adi', aliasdomain_info, 'fqdn', - _('show the destination of the given alias domain')), - 'aliasdomainswitch': cmd('aliasdomainswitch', 'ads', aliasdomain_switch, - 'fqdn destination', _('assign the given alias ' - 'domain to an other domain')), - # CatchallAlias commands - 'catchalladd': cmd('catchalladd', 'caa', catchall_add, - 'fqdn destination ...', - _('add one or more catch-all destinations for a ' - 'domain')), - 'catchalldelete': cmd('catchalldelete', 'cad', catchall_delete, - 'fqdn [destination ...]', - _('delete the specified catch-all destination or all ' - 'of a domain\'s destinations')), - 'catchallinfo': cmd('catchallinfo', 'cai', catchall_info, 'fqdn', - _('show the catch-all destination(s) of the ' - 'specified domain')), - # Domain commands - 'domainadd': cmd('domainadd', 'da', domain_add, 'fqdn [transport]', - _('create a new domain')), - 'domaindelete': cmd('domaindelete', 'dd', domain_delete, 'fqdn [force]', - _('delete the given domain and all its alias domains')), - 'domaininfo': cmd('domaininfo', 'di', domain_info, 'fqdn [details]', - _('display information about the given domain')), - 'domainquota': cmd('domainquota', 'dq', domain_quota, - 'fqdn storage [messages] [force]', - _('update the quota limit of the specified domain')), - 'domainservices': cmd('domainservices', 'ds', domain_services, - 'fqdn [service ...] [force]', - _('enables the specified services and disables all ' - 'not specified services of the given domain')), - 'domaintransport': cmd('domaintransport', 'dt', domain_transport, - 'fqdn transport [force]', - _('update the transport of the specified domain')), - 'domainnote': cmd('domainnote', 'do', domain_note, 'fqdn [note]', - _('set, update or delete the note of the given domain')), - # List commands - 'listdomains': cmd('listdomains', 'ld', list_domains, '[pattern]', - _('list all domains or search for domains by pattern')), - 'listaddresses': cmd('listaddresses', 'll', list_addresses, '[pattern]', - _('list all addresses or search for addresses by ' - 'pattern')), - 'listusers': cmd('listusers', 'lu', list_users, '[pattern]', - _('list all user accounts or search for accounts by ' - 'pattern')), - 'listaliases': cmd('listaliases', 'la', list_aliases, '[pattern]', - _('list all aliases or search for aliases by pattern')), - 'listrelocated': cmd('listrelocated', 'lr', list_relocated, '[pattern]', - _('list all relocated users or search for relocated ' - 'users by pattern')), - # Relocated commands - 'relocatedadd': cmd('relocatedadd', 'ra', relocated_add, - 'address newaddress', - _('create a new record for a relocated user')), - 'relocateddelete': cmd('relocateddelete', 'rd', relocated_delete, - 'address', - _('delete the record of the relocated user')), - 'relocatedinfo': cmd('relocatedinfo', 'ri', relocated_info, 'address', - _('print information about a relocated user')), - # cli commands - 'configget': cmd('configget', 'cg', config_get, 'option', - _('show the actual value of the configuration option')), - 'configset': cmd('configset', 'cs', config_set, 'option value', - _('set a new value for the configuration option')), - 'configure': cmd('configure', 'cf', configure, '[section]', - _('start interactive configuration mode')), - 'listpwschemes': cmd('listpwschemes', 'lp', list_pwschemes, '', - _('lists all usable password schemes and password ' - 'encoding suffixes')), - 'help': cmd('help', 'h', help_, '[subcommand]', - _('show a help overview or help for the given subcommand')), - 'version': cmd('version', 'v', version, '', - _('show version and copyright information')), - }) +def setup_parser(): + """Create the argument parser, add all the subcommands and return it.""" + class ArgParser(ArgumentParser): + """This class fixes the 'width detection'.""" + def _get_formatter(self): + return self.formatter_class(prog=self.prog, width=WS_ROWS, + max_help_position=26) + + class VersionAction(Action): + """Show version and copyright information.""" + def __call__(self, parser, namespace, values, option_string=None): + """implements the Action API.""" + vers_info = _('{program}, version {version} (from {rel_date})\n' + 'Python {py_vers} on {sysname}'.format( + program=parser.prog, version=__version__, + rel_date=strftime( + locale.nl_langinfo(locale.D_FMT), + strptime(__date__, '%Y-%m-%d')), + py_vers=platform.python_version(), + sysname=platform.system())) + copy_info = _('{copyright}\n{program} is free software and comes ' + 'with ABSOLUTELY NO WARRANTY.'.format( + copyright=__copyright__, program=parser.prog)) + parser.exit(message='\n\n'.join((vers_info, copy_info)) + '\n') + + def quota_storage(string): + if string == 'domain': + return string + try: + storage = size_in_bytes(string) + except (TypeError, ValueError) as error: + raise ArgumentTypeError(str(error)) + return storage + + old_rw = txt_wrpr.replace_whitespace + txt_wrpr.replace_whitespace = False + fill = lambda t: '\n'.join(txt_wrpr.fill(l) for l in t.splitlines(True)) + mklst = lambda iterable: '\n\t - ' + '\n\t - '.join(iterable) + + description = _('%(prog)s - command line tool to manage email ' + 'domains/accounts/aliases/...') + epilog = _('use "%(prog)s -h" for information about the ' + 'given subcommand') + parser = ArgParser(description=description, epilog=epilog) + parser.add_argument('-v', '--version', action=VersionAction, nargs=0, + help=_("show %(prog)s's version and copyright " + "information and exit")) + subparsers = parser.add_subparsers(metavar=_(''), + title=_('list of available subcommands')) + a = subparsers.add_parser + + ### + # general subcommands + ### + cg = a('configget', aliases=('cg',), + help=_('show the actual value of the configuration option'), + epilog=_("This subcommand is used to display the actual value of " + "the given configuration option.")) + cg.add_argument('option', help=_('the name of a configuration option')) + cg.set_defaults(func=config_get, scmd='configget') + + cs = a('configset', aliases=('cs',), + help=_('set a new value for the configuration option'), + epilog=fill(_("Use this subcommand to set or update a single " + "configuration option's value. option is the configuration " + "option, value is the option's new value.\n\nNote: This " + "subcommand will create a new vmm.cfg without any comments. " + "Your current configuration file will be backed as " + "vmm.cfg.bak.")), + formatter_class=RawDescriptionHelpFormatter) + cs.add_argument('option', help=_('the name of a configuration option')) + cs.add_argument('value', help=_("the option's new value")) + cs.set_defaults(func=config_set, scmd='configset') + + sections = ('account', 'bin', 'database', 'domain', 'mailbox', 'misc') + cf = a('configure', aliases=('cf',), + help=_('start interactive configuration mode'), + epilog=fill(_("Starts the interactive configuration for all " + "configuration sections.\n\nIn this process the currently set " + "value of each option will be displayed in square brackets. " + "If no value is configured, the default value of each option " + "will be displayed in square brackets. Press the return key, " + "to accept the displayed value.\n\n" + "If the optional argument section is given, only the " + "configuration options from the given section will be " + "displayed and will be configurable. The following sections " + "are available:\n") + mklst(sections)), + formatter_class=RawDescriptionHelpFormatter) + cf.add_argument('-s', choices=sections, metavar='SECTION', dest='section', + help=_("configure only options of the given section")) + cf.set_defaults(func=configure, scmd='configure') + + gu = a('getuser', aliases=('gu',), + help=_('get the address of the user with the given UID'), + epilog=_("If only the uid is available, for example from process " + "list, the subcommand getuser will show the user's " + "address.")) + gu.add_argument('uid', type=int, help=_("a user's unique identifier")) + gu.set_defaults(func=get_user, scmd='getuser') + + ll = a('listaddresses', aliases=('ll',), + help=_('list all addresses or search for addresses by pattern'), + epilog=fill(_("This command lists all defined addresses. " + "Addresses belonging to alias-domains are prefixed with a '-', " + "addresses of regular domains with a '+'. Additionally, the " + "letters 'u', 'a', and 'r' indicate the type of each address: " + "user, alias and relocated respectively. The output can be " + "limited with an optional pattern.\n\nTo perform a wild card " + "search, the % character can be used at the start and/or the " + "end of the pattern.")), + formatter_class=RawDescriptionHelpFormatter) + ll.add_argument('-p', help=_("the pattern to search for"), + metavar='PATTERN', dest='pattern') + ll.set_defaults(func=list_addresses, scmd='listaddresses') + + la = a('listaliases', aliases=('la',), + help=_('list all aliases or search for aliases by pattern'), + epilog=fill(_("This command lists all defined aliases. Aliases " + "belonging to alias-domains are prefixed with a '-', addresses " + "of regular domains with a '+'. The output can be limited " + "with an optional pattern.\n\nTo perform a wild card search, " + "the % character can be used at the start and/or the end of " + "the pattern.")), + formatter_class=RawDescriptionHelpFormatter) + la.add_argument('-p', help=_("the pattern to search for"), + metavar='PATTERN', dest='pattern') + la.set_defaults(func=list_aliases, scmd='listaliases') + + ld = a('listdomains', aliases=('ld',), + help=_('list all domains or search for domains by pattern'), + epilog=fill(_("This subcommand lists all available domains. All " + "domain names will be prefixed either with `[+]', if the " + "domain is a primary domain, or with `[-]', if it is an alias " + "domain name. The output can be limited with an optional " + "pattern.\n\nTo perform a wild card search, the % character " + "can be used at the start and/or the end of the pattern.")), + formatter_class=RawDescriptionHelpFormatter) + ld.add_argument('-p', help=_("the pattern to search for"), + metavar='PATTERN', dest='pattern') + ld.set_defaults(func=list_domains, scmd='listdomains') + + lr = a('listrelocated', aliases=('lr',), + help=_('list all relocated users or search for relocated users by ' + 'pattern'), + epilog=fill(_("This command lists all defined relocated addresses. " + "Relocated entries belonging to alias-domains are prefixed " + "with a '-', addresses of regular domains with a '+'. The " + "output can be limited with an optional pattern.\n\nTo " + "perform a wild card search, the % character can be used at " + "the start and/or the end of the pattern.")), + formatter_class=RawDescriptionHelpFormatter) + lr.add_argument('-p', help=_("the pattern to search for"), + metavar='PATTERN', dest='pattern') + lr.set_defaults(func=list_relocated, scmd='listrelocated') + + lu = a('listusers', aliases=('lu',), + help=_('list all user accounts or search for accounts by pattern'), + epilog=fill(_("This command lists all user accounts. User accounts " + "belonging to alias-domains are prefixed with a '-', " + "addresses of regular domains with a '+'. The output can be " + "limited with an optional pattern.\n\nTo perform a wild card " + "search, the % character can be used at the start and/or the " + "end of the pattern.")), + formatter_class=RawDescriptionHelpFormatter) + lu.add_argument('-p', help=_("the pattern to search for"), + metavar='PATTERN', dest='pattern') + lu.set_defaults(func=list_users, scmd='listusers') + + lp = a('listpwschemes', aliases=('lp',), + help=_('lists all usable password schemes and password encoding ' + 'suffixes'), + epilog=fill(_("This subcommand lists all password schemes which " + "could be used in the vmm.cfg as value of the " + "misc.password_scheme option. The output varies, depending " + "on the used Dovecot version and the system's libc.\nWhen " + "your Dovecot installation isn't too old, you will see " + "additionally a few usable encoding suffixes. One of them can " + "be appended to the password scheme.")), + formatter_class=RawDescriptionHelpFormatter) + lp.set_defaults(func=list_pwschemes, scmd='listpwschemes') + + ### + # domain subcommands + ### + da = a('domainadd', aliases=('da',), help=_('create a new domain'), + epilog=fill(_("Adds the new domain into the database and creates " + "the domain directory.\n\nIf the optional argument transport " + "is given, it will override the default transport " + "(domain.transport) from vmm.cfg. The specified transport " + "will be the default transport for all new accounts in this " + "domain.")), + formatter_class=RawDescriptionHelpFormatter) + da.add_argument('fqdn', help=_('a fully qualified domain name')) + da.add_argument('-t', metavar='TRANSPORT', dest='transport', + help=_('a Postfix transport (transport: or ' + 'transport:nexthop)')) + da.set_defaults(func=domain_add, scmd='domainadd') + + details = ('accounts', 'aliasdomains', 'aliases', 'catchall', 'relocated', + 'full') + di = a('domaininfo', aliases=('di',), + help=_('display information about the given domain'), + epilog=fill(_("This subcommand shows some information about the " + "given domain.\n\nFor a more detailed information about the " + "domain the optional argument details can be specified. A " + "possible details value can be one of the following six " + "keywords:\n") + mklst(details)), + formatter_class=RawDescriptionHelpFormatter) + di.add_argument('fqdn', help=_('a fully qualified domain name')) + di.add_argument('-d', choices=details, dest='details', metavar='DETAILS', + help=_('additionally details to display')) + di.set_defaults(func=domain_info, scmd='domaininfo') + + do = a('domainnote', aliases=('do',), + help=_('set, update or delete the note of the given domain'), + epilog=_('With this subcommand, it is possible to attach a note to ' + 'the specified domain. Without an argument, an existing ' + 'note is removed.')) + do.add_argument('fqdn', help=_('a fully qualified domain name')) + do.add_argument('-n', metavar='NOTE', dest='note', + help=_('the note that should be set')) + do.set_defaults(func=domain_note, scmd='domainnote') + + dq = a('domainquota', aliases=('dq',), + help=_('update the quota limit of the specified domain'), + epilog=fill(_("This subcommand is used to configure a new quota " + "limit for the accounts of the domain - not for the domain " + "itself.\n\nThe default quota limit for accounts is defined " + "in the vmm.cfg (domain.quota_bytes and " + "domain.quota_messages).\n\nThe new quota limit will affect " + "only those accounts for which the limit has not been " + "overridden. If you want to restore the default to all " + "accounts, you may pass the optional argument --force. When " + "the argument messages was omitted the default number of " + "messages 0 (zero) will be applied.")), + formatter_class=RawDescriptionHelpFormatter) + dq.add_argument('fqdn', help=_('a fully qualified domain name')) + dq.add_argument('storage', type=quota_storage, + help=_('quota limit in {kilo,mega,giga}bytes e.g. 2G ' + 'or 2048M',)) + dq.add_argument('-m', default=0, type=int, metavar='MESSAGES', + dest='messages', + help=_('quota limit in number of messages (default: 0)')) + dq.add_argument('--force', action='store_true', + help=_('enforce the limit for all accounts')) + dq.set_defaults(func=domain_quota, scmd='domainquota') + + ds = a('domainservices', aliases=('ds',), + help=_('enables the specified services and disables all not ' + 'specified services of the given domain'), + epilog=fill(_("To define which services could be used by the users " + "of the domain — with the given fqdn — use this " + "subcommand.\n\nEach specified service will be enabled/" + "usable. All other services will be deactivated/unusable. " + "Possible service names are: imap, pop3, sieve and smtp.\nThe " + "new service set will affect only those accounts for which " + "the set has not been overridden. If you want to restore the " + "default to all accounts, you may pass --force.")), + formatter_class=RawDescriptionHelpFormatter) + ds.add_argument('fqdn', help=_('a fully qualified domain name')) + ds.add_argument('-s', choices=SERVICES, + help=_('services which should be usable'), + metavar='SERVICE', nargs='+', dest='services') + ds.add_argument('--force', action='store_true', + help=_('enforce the service set for all accounts')) + ds.set_defaults(func=domain_services, scmd='domainservices') + + dt = a('domaintransport', aliases=('dt',), + help=_('update the transport of the specified domain'), + epilog=fill(_("A new transport for the indicated domain can be set " + "with this subcommand.\n\nThe new transport will affect only " + "those accounts for which the transport has not been " + "overridden. If you want to restore the default to all " + "accounts, you may pass --force.")), + formatter_class=RawDescriptionHelpFormatter) + dt.add_argument('fqdn', help=_('a fully qualified domain name')) + dt.add_argument('transport', help=_('a Postfix transport (transport: or ' + 'transport:nexthop)')) + dt.add_argument('--force', action='store_true', + help=_('enforce the transport for all accounts')) + dt.set_defaults(func=domain_transport, scmd='domaintransport') + + dd = a('domaindelete', aliases=('dd',), + help=_('delete the given domain and all its alias domains'), + epilog=fill(_("This subcommand deletes the domain specified by " + "fqdn.\n\nIf there are accounts, aliases and/or relocated " + "users assigned to the given domain, vmm will abort the " + "requested operation and show an error message. If you know, " + "what you are doing, you can specify the optional argument " + "--force.\n\nIf you really always know what you are doing, " + "edit your vmm.cfg and set the option domain.force_deletion " + "to true.")), + formatter_class=RawDescriptionHelpFormatter) + dd.add_argument('fqdn', help=_('a fully qualified domain name')) + dd.add_argument('--force', action='store_true', + help=_('also delete all accounts, aliases and/or ' + 'relocated users')) + dd.set_defaults(func=domain_delete, scmd='domaindelete') + + ### + # alias domain subcommands + ### + ada = a('aliasdomainadd', aliases=('ada',), + help=_('create a new alias for an existing domain'), + epilog=_('This subcommand adds the new alias domain (fqdn) to ' + 'the destination domain that should be aliased.')) + ada.add_argument('fqdn', help=_('a fully qualified domain name')) + ada.add_argument('destination', + help=_('the fqdn of the destination domain')) + ada.set_defaults(func=aliasdomain_add, scmd='aliasdomainadd') + + adi = a('aliasdomaininfo', aliases=('adi',), + help=_('show the destination of the given alias domain'), + epilog=_('This subcommand shows to which domain the alias domain ' + 'fqdn is assigned to.')) + adi.add_argument('fqdn', help=_('a fully qualified domain name')) + adi.set_defaults(func=aliasdomain_info, scmd='aliasdomaininfo') + + ads = a('aliasdomainswitch', aliases=('ads',), + help=_('assign the given alias domain to an other domain'), + epilog=_('If the destination of the existing alias domain fqdn ' + 'should be switched to another destination use this ' + 'subcommand.')) + ads.add_argument('fqdn', help=_('a fully qualified domain name')) + ads.add_argument('destination', + help=_('the fqdn of the destination domain')) + ads.set_defaults(func=aliasdomain_switch, scmd='aliasdomainswitch') + + add = a('aliasdomaindelete', aliases=('add',), + help=_('delete the specified alias domain'), + epilog=_('Use this subcommand if the alias domain fqdn should be ' + 'removed.')) + add.add_argument('fqdn', help=_('a fully qualified domain name')) + add.set_defaults(func=aliasdomain_delete, scmd='aliasdomaindelete') + + ### + # account subcommands + ### + ua = a('useradd', aliases=('ua',), + help=_('create a new e-mail user with the given address'), + epilog=fill(_('Use this subcommand to create a new e-mail account ' + 'for the given address.\n\nIf the password is not provided, ' + 'vmm will prompt for it interactively. When no password is ' + 'provided and account.random_password is set to true, vmm ' + 'will generate a random password and print it to stdout ' + 'after the account has been created.')), + formatter_class=RawDescriptionHelpFormatter) + ua.add_argument('address', + help=_("an account's e-mail address (local-part@fqdn)")) + ua.add_argument('-p', metavar='PASSWORD', dest='password', + help=_("the new user's password")) + ua.set_defaults(func=user_add, scmd='useradd') + + details = ('aliases', 'du', 'full') + ui = a('userinfo', aliases=('ui',), + help=_('display information about the given address'), + epilog=fill(_('This subcommand displays some information about ' + 'the account specified by the given address.\n\nIf the ' + 'optional argument details is given some more information ' + 'will be displayed.\nPossible values for details are:\n') + + mklst(details)), + formatter_class=RawDescriptionHelpFormatter) + ui.add_argument('address', + help=_("an account's e-mail address (local-part@fqdn)")) + ui.add_argument('-d', choices=details, metavar='DETAILS', dest='details', + help=_('additionally details to display')) + ui.set_defaults(func=user_info, scmd='userinfo') + + un = a('username', aliases=('un',), + help=_('set, update or delete the real name for an address'), + epilog=fill(_("The user's real name can be set/updated with this " + "subcommand.\n\nIf no name is given, the value stored for the " + "account is erased.")), + formatter_class=RawDescriptionHelpFormatter) + un.add_argument('address', + help=_("an account's e-mail address (local-part@fqdn)")) + un.add_argument('-n', help=_("a user's real name"), metavar='NAME', + dest='name') + un.set_defaults(func=user_name, scmd='username') + + uo = a('usernote', aliases=('uo',), + help=_('set, update or delete the note of the given address'), + epilog=_('With this subcommand, it is possible to attach a note to ' + 'the specified account. Without the note argument, an ' + 'existing note is removed.')) + uo.add_argument('address', + help=_("an account's e-mail address (local-part@fqdn)")) + uo.add_argument('-n', metavar='NOTE', dest='note', + help=_('the note that should be set')) + uo.set_defaults(func=user_note, scmd='usernote') + + up = a('userpassword', aliases=('up',), + help=_('update the password for the given address'), + epilog=fill(_("The password of an account can be updated with this " + "subcommand.\n\nIf no password was provided, vmm will prompt " + "for it interactively.")), + formatter_class=RawDescriptionHelpFormatter) + up.add_argument('address', + help=_("an account's e-mail address (local-part@fqdn)")) + up.add_argument('-p', metavar='PASSWORD', dest='password', + help=_("the user's new password")) + up.set_defaults(func=user_password, scmd='userpassword') + + uq = a('userquota', aliases=('uq',), + help=_('update the quota limit for the given address'), + epilog=fill(_("This subcommand is used to set a new quota limit " + "for the given account.\n\nWhen the argument messages was " + "omitted the default number of messages 0 (zero) will be " + "applied.\n\nInstead of a storage limit pass the keyword " + "'domain' to remove the account-specific override, causing " + "the domain's value to be in effect.")), + formatter_class=RawDescriptionHelpFormatter) + uq.add_argument('address', + help=_("an account's e-mail address (local-part@fqdn)")) + uq.add_argument('storage', type=quota_storage, + help=_('quota limit in {kilo,mega,giga}bytes e.g. 2G ' + 'or 2048M')) + uq.add_argument('-m', default=0, type=int, metavar='MESSAGES', + dest='messages', + help=_('quota limit in number of messages (default: 0)')) + uq.set_defaults(func=user_quota, scmd='userquota') + + us = a('userservices', aliases=('us',), + help=_('enable the specified services and disables all not ' + 'specified services'), + epilog=fill(_("To grant a user access to the specified service(s), " + "use this command.\n\nAll omitted services will be " + "deactivated/unusable for the user with the given " + "address.\n\nInstead of any service pass the keyword " + "'domain' to remove the account-specific override, causing " + "the domain's value to be in effect.")), + formatter_class=RawDescriptionHelpFormatter) + us.add_argument('address', + help=_("an account's e-mail address (local-part@fqdn)")) + us.add_argument('-s', choices=SERVICES + ('domain',), + help=_('services which should be usable'), + metavar='SERVICE', nargs='+', dest='services') + us.set_defaults(func=user_services, scmd='userservices') + + ut = a('usertransport', aliases=('ut',), + help=_('update the transport of the given address'), + epilog=fill(_("A different transport for an account can be " + "specified with this subcommand.\n\nInstead of a transport " + "pass the keyword 'domain' to remove the account-specific " + "override, causing the domain's value to be in effect.")), + formatter_class=RawDescriptionHelpFormatter) + ut.add_argument('address', + help=_("an account's e-mail address (local-part@fqdn)")) + ut.add_argument('transport', help=_('a Postfix transport (transport: or ' + 'transport:nexthop)')) + ut.set_defaults(func=user_transport, scmd='usertransport') + + ud = a('userdelete', aliases=('ud',), + help=_('delete the specified user'), + epilog=fill(_('Use this subcommand to delete the account with the ' + 'given address.\n\nIf there are one or more aliases with an ' + 'identical destination address, vmm will abort the requested ' + 'operation and show an error message. To prevent this, ' + 'give the optional argument --force.')), + formatter_class=RawDescriptionHelpFormatter) + ud.add_argument('address', + help=_("an account's e-mail address (local-part@fqdn)")) + ud.add_argument('--force', action='store_true', + help=_('also delete assigned alias addresses')) + ud.set_defaults(func=user_delete, scmd='userdelete') + + ### + # alias subcommands + ### + aa = a('aliasadd', aliases=('aa',), + help=_('create a new alias e-mail address with one or more ' + 'destinations'), + epilog=fill(_("This subcommand is used to create a new alias " + "address with one or more destination addresses.\n\nWithin " + "the destination address, the placeholders %n, %d, and %= " + "will be replaced by the local part, the domain, or the " + "email address with '@' replaced by '=' respectively. In " + "combination with alias domains, this enables " + "domain-specific destinations.")), + formatter_class=RawDescriptionHelpFormatter) + aa.add_argument('address', + help=_("an alias' e-mail address (local-part@fqdn)")) + aa.add_argument('destination', nargs='+', + help=_("a destination's e-mail address (local-part@fqdn)")) + aa.set_defaults(func=alias_add, scmd='aliasadd') + + ai = a('aliasinfo', aliases=('ai',), + help=_('show the destination(s) of the specified alias'), + epilog=_('Information about the alias with the given address can ' + 'be displayed with this subcommand.')) + ai.add_argument('address', + help=_("an alias' e-mail address (local-part@fqdn)")) + ai.set_defaults(func=alias_info, scmd='aliasinfo') + + ad = a('aliasdelete', aliases=('ad',), + help=_('delete the specified alias e-mail address or one of its ' + 'destinations'), + epilog=fill(_("This subcommand is used to delete one or multiple " + "destinations from the alias with the given address.\n\nWhen " + "no destination address was specified the alias with all its " + "destinations will be deleted.")), + formatter_class=RawDescriptionHelpFormatter) + ad.add_argument('address', + help=_("an alias' e-mail address (local-part@fqdn)")) + ad.add_argument('destination', nargs='*', + help=_("a destination's e-mail address (local-part@fqdn)")) + ad.set_defaults(func=alias_delete, scmd='aliasdelete') + + ### + # catch-all subcommands + ### + caa = a('catchalladd', aliases=('caa',), + help=_('add one or more catch-all destinations for a domain'), + epilog=fill(_('This subcommand allows to specify destination ' + 'addresses for a domain, which shall receive mail addressed ' + 'to unknown local parts within that domain. Those catch-all ' + 'aliases hence "catch all" mail to any address in the domain ' + '(unless a more specific alias, mailbox or relocated entry ' + 'exists).\n\nWARNING: Catch-all addresses can cause mail ' + 'server flooding because spammers like to deliver mail to ' + 'all possible combinations of names, e.g. to all addresses ' + 'between abba@example.org and zztop@example.org.')), + formatter_class=RawDescriptionHelpFormatter) + caa.add_argument('fqdn', help=_('a fully qualified domain name')) + caa.add_argument('destination', nargs='+', + help=_("a destination's e-mail address (local-part@fqdn)")) + caa.set_defaults(func=catchall_add, scmd='catchalladd') + + cai = a('catchallinfo', aliases=('cai',), + help=_('show the catch-all destination(s) of the specified ' + 'domain'), + epilog=_('This subcommand displays information about catch-all ' + 'aliases defined for a domain.')) + cai.add_argument('fqdn', help=_('a fully qualified domain name')) + cai.set_defaults(func=catchall_info, scmd='catchallinfo') + + cad = a('catchalldelete', aliases=('cad',), + help=_("delete the specified catch-all destination or all of a " + "domain's destinations"), + epilog=_('With this subcommand, catch-all aliases defined for a ' + 'domain can be removed, either all of them, or those ' + 'destinations which were specified explicitly.')) + cad.add_argument('fqdn', help=_('a fully qualified domain name')) + cad.add_argument('destination', nargs='*', + help=_("a destination's e-mail address (local-part@fqdn)")) + cad.set_defaults(func=catchall_delete, scmd='catchalldelete') + + ### + # relocated subcommands + ### + ra = a('relocatedadd', aliases=('ra',), + help=_('create a new record for a relocated user'), + epilog=_("A new relocated user can be created with this " + "subcommand.")) + ra.add_argument('address', help=_("a relocated user's e-mail address " + "(local-part@fqdn)")) + ra.add_argument('newaddress', + help=_('e-mail address where the user can be reached now')) + ra.set_defaults(func=relocated_add, scmd='relocatedadd') + + ri = a('relocatedinfo', aliases=('ri',), + help=_('print information about a relocated user'), + epilog=_('This subcommand shows the new address of the relocated ' + 'user with the given address.')) + ri.add_argument('address', help=_("a relocated user's e-mail address " + "(local-part@fqdn)")) + ri.set_defaults(func=relocated_info, scmd='relocatedinfo') + + rd = a('relocateddelete', aliases=('rd',), + help=_('delete the record of the relocated user'), + epilog=_('Use this subcommand in order to delete the relocated ' + 'user with the given address.')) + rd.add_argument('address', help=_("a relocated user's e-mail address " + "(local-part@fqdn)")) + rd.set_defaults(func=relocated_delete, scmd='relocateddelete') + + txt_wrpr.replace_whitespace = old_rw + return parser def _get_order(ctx): """returns a tuple with (key, 1||0) tuples. Used by functions, which get a dict from the handler.""" order = () - if ctx.scmd == 'domaininfo': + if ctx.args.scmd == 'domaininfo': order = (('domain name', 0), ('gid', 1), ('domain directory', 0), ('quota limit/user', 0), ('active services', 0), ('transport', 0), ('alias domains', 0), ('accounts', 0), ('aliases', 0), ('relocated', 0), ('catch-all dests', 0)) - elif ctx.scmd == 'userinfo': - if ctx.argc == 4 and ctx.args[3] != 'aliases' or \ + elif ctx.args.scmd == 'userinfo': + if ctx.args.details in ('du', 'full') or \ ctx.cget('account.disk_usage'): order = (('address', 0), ('name', 0), ('uid', 1), ('gid', 1), ('home', 0), ('mail_location', 0), @@ -927,7 +1028,7 @@ ('quota storage', 0), ('quota messages', 0), ('transport', 0), ('smtp', 1), ('pop3', 1), ('imap', 1), ('sieve', 1)) - elif ctx.scmd == 'getuser': + elif ctx.args.scmd == 'getuser': order = (('uid', 1), ('gid', 1), ('address', 0)) return order