VirtualMailManager/cli/subcommands.py
branchv0.7.x
changeset 665 33d15936b53a
parent 657 6515e3b88dec
child 676 2bc11dada296
equal deleted inserted replaced
664:b5cc967a45ad 665:33d15936b53a
     7 
     7 
     8     VirtualMailManager's cli subcommands.
     8     VirtualMailManager's cli subcommands.
     9 """
     9 """
    10 
    10 
    11 import locale
    11 import locale
    12 import os
    12 import platform
    13 
    13 
       
    14 from argparse import Action, ArgumentParser, ArgumentTypeError, \
       
    15      RawDescriptionHelpFormatter
    14 from textwrap import TextWrapper
    16 from textwrap import TextWrapper
    15 from time import strftime, strptime
    17 from time import strftime, strptime
    16 
    18 
    17 from VirtualMailManager import ENCODING
    19 from VirtualMailManager import ENCODING
    18 from VirtualMailManager.cli import get_winsize, prog, w_err, w_std
    20 from VirtualMailManager.cli import get_winsize, w_err, w_std
    19 from VirtualMailManager.cli.clihelp import help_msgs
       
    20 from VirtualMailManager.common import human_size, size_in_bytes, \
    21 from VirtualMailManager.common import human_size, size_in_bytes, \
    21      version_str, format_domain_default
    22      version_str, format_domain_default
    22 from VirtualMailManager.constants import __copyright__, __date__, \
    23 from VirtualMailManager.constants import __copyright__, __date__, \
    23      __version__, ACCOUNT_EXISTS, ALIAS_EXISTS, ALIASDOMAIN_ISDOMAIN, \
    24      __version__, ACCOUNT_EXISTS, ALIAS_EXISTS, ALIASDOMAIN_ISDOMAIN, \
    24      DOMAIN_ALIAS_EXISTS, INVALID_ARGUMENT, EX_MISSING_ARGS, \
    25      DOMAIN_ALIAS_EXISTS, INVALID_ARGUMENT, RELOCATED_EXISTS, TYPE_ACCOUNT, \
    25      RELOCATED_EXISTS, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED
    26      TYPE_ALIAS, TYPE_RELOCATED
    26 from VirtualMailManager.errors import VMMError
    27 from VirtualMailManager.errors import VMMError
    27 from VirtualMailManager.password import list_schemes
    28 from VirtualMailManager.password import list_schemes
    28 from VirtualMailManager.serviceset import SERVICES
    29 from VirtualMailManager.serviceset import SERVICES
    29 
    30 
    30 __all__ = (
    31 __all__ = (
    31     'Command', 'RunContext', 'cmd_map', 'usage', 'alias_add', 'alias_delete',
    32     'RunContext', 'alias_add', 'alias_delete', 'alias_info', 'aliasdomain_add',
    32     'alias_info', 'aliasdomain_add', 'aliasdomain_delete', 'aliasdomain_info',
    33     'aliasdomain_delete', 'aliasdomain_info', 'aliasdomain_switch',
    33     'aliasdomain_switch', 'catchall_add', 'catchall_info', 'catchall_delete',
    34     'catchall_add', 'catchall_delete', 'catchall_info', 'config_get',
    34     'config_get', 'config_set', 'configure',
    35     'config_set', 'configure', 'domain_add', 'domain_delete', 'domain_info',
    35     'domain_add', 'domain_delete',  'domain_info', 'domain_quota',
    36     'domain_note', 'domain_quota', 'domain_services', 'domain_transport',
    36     'domain_services', 'domain_transport', 'domain_note', 'get_user', 'help_',
    37     'get_user', 'list_addresses', 'list_aliases', 'list_domains',
    37     'list_domains', 'list_pwschemes', 'list_users', 'list_aliases',
    38     'list_pwschemes', 'list_relocated', 'list_users', 'relocated_add',
    38     'list_relocated', 'list_addresses', 'relocated_add', 'relocated_delete',
    39     'relocated_delete', 'relocated_info', 'setup_parser', 'user_add',
    39     'relocated_info', 'user_add', 'user_delete', 'user_info', 'user_name',
    40     'user_delete', 'user_info', 'user_name', 'user_note', 'user_password',
    40     'user_password', 'user_quota', 'user_services', 'user_transport',
    41     'user_quota', 'user_services', 'user_transport',
    41     'user_note', 'version',
       
    42 )
    42 )
    43 
    43 
       
    44 WS_ROWS = get_winsize()[1] - 2
       
    45 
    44 _ = lambda msg: msg
    46 _ = lambda msg: msg
    45 txt_wrpr = TextWrapper(width=get_winsize()[1] - 1)
    47 txt_wrpr = TextWrapper(width=WS_ROWS)
    46 cmd_map = {}
       
    47 
       
    48 
       
    49 class Command(object):
       
    50     """Container class for command information."""
       
    51     __slots__ = ('name', 'alias', 'func', 'args', 'descr')
       
    52     FMT_HLP_USAGE = """
       
    53 usage: %(prog)s %(name)s %(args)s
       
    54        %(prog)s %(alias)s %(args)s
       
    55 """
       
    56 
       
    57     def __init__(self, name, alias, func, args, descr):
       
    58         """Create a new Command instance.
       
    59 
       
    60         Arguments:
       
    61 
       
    62         `name` : str
       
    63           the command name, e.g. ``addalias``
       
    64         `alias` : str
       
    65           the command's short alias, e.g. ``aa``
       
    66         `func` : callable
       
    67           the function to handle the command
       
    68         `args` : str
       
    69           argument placeholders, e.g. ``aliasaddress``
       
    70         `descr` : str
       
    71           short description of the command
       
    72         """
       
    73         self.name = name
       
    74         self.alias = alias
       
    75         self.func = func
       
    76         self.args = args
       
    77         self.descr = descr
       
    78 
       
    79     @property
       
    80     def usage(self):
       
    81         """the command's usage info."""
       
    82         return '%s %s %s' % (prog, self.name, self.args)
       
    83 
       
    84     def help_(self):
       
    85         """Print the Command's help message to stdout."""
       
    86         old_ii = txt_wrpr.initial_indent
       
    87         old_si = txt_wrpr.subsequent_indent
       
    88 
       
    89         txt_wrpr.subsequent_indent = (len(self.name) + 2) * ' '
       
    90         w_std(txt_wrpr.fill('%s: %s' % (self.name, self.descr)))
       
    91 
       
    92         info = Command.FMT_HLP_USAGE % dict(alias=self.alias, args=self.args,
       
    93                                             name=self.name, prog=prog)
       
    94         w_std(info)
       
    95 
       
    96         txt_wrpr.initial_indent = txt_wrpr.subsequent_indent = ' '
       
    97         try:
       
    98             [w_std(txt_wrpr.fill(_(para)) + '\n') for para
       
    99                     in help_msgs[self.name]]
       
   100         except KeyError:
       
   101             w_err(1, _("Subcommand '%s' is not yet documented." % self.name),
       
   102                   'see also: vmm(1)')
       
   103 
    48 
   104 
    49 
   105 class RunContext(object):
    50 class RunContext(object):
   106     """Contains all information necessary to run a subcommand."""
    51     """Contains all information necessary to run a subcommand."""
   107     __slots__ = ('argc', 'args', 'cget', 'hdlr', 'scmd')
    52     __slots__ = ('args', 'cget', 'hdlr')
   108     plan_a_b = _('Plan A failed ... trying Plan B: %(subcommand)s %(object)s')
    53     plan_a_b = _('Plan A failed ... trying Plan B: %(subcommand)s %(object)s')
   109 
    54 
   110     def __init__(self, argv, handler, command):
    55     def __init__(self, args, handler):
   111         """Create a new RunContext"""
    56         """Create a new RunContext"""
   112         self.argc = len(argv)
    57         self.args = args
   113         self.args = argv[:]  # will be moved to argparse
       
   114         self.cget = handler.cfg_dget
    58         self.cget = handler.cfg_dget
   115         self.hdlr = handler
    59         self.hdlr = handler
   116         self.scmd = command
       
   117 
    60 
   118 
    61 
   119 def alias_add(ctx):
    62 def alias_add(ctx):
   120     """create a new alias e-mail address"""
    63     """create a new alias e-mail address"""
   121     if ctx.argc < 3:
    64     ctx.hdlr.alias_add(ctx.args.address.lower(), *ctx.args.destination)
   122         usage(EX_MISSING_ARGS, _('Missing alias address and destination.'),
       
   123               ctx.scmd)
       
   124     elif ctx.argc < 4:
       
   125         usage(EX_MISSING_ARGS, _('Missing destination address.'), ctx.scmd)
       
   126     ctx.hdlr.alias_add(ctx.args[2].lower(), *ctx.args[3:])
       
   127 
    65 
   128 
    66 
   129 def alias_delete(ctx):
    67 def alias_delete(ctx):
   130     """delete the specified alias e-mail address or one of its destinations"""
    68     """delete the specified alias e-mail address or one of its destinations"""
   131     if ctx.argc < 3:
    69     destination = ctx.args.destination if ctx.args.destination else None
   132         usage(EX_MISSING_ARGS, _('Missing alias address.'), ctx.scmd)
    70     ctx.hdlr.alias_delete(ctx.args.address.lower(), destination)
   133     elif ctx.argc < 4:
       
   134         ctx.hdlr.alias_delete(ctx.args[2].lower())
       
   135     else:
       
   136         ctx.hdlr.alias_delete(ctx.args[2].lower(), ctx.args[3:])
       
   137 
    71 
   138 
    72 
   139 def alias_info(ctx):
    73 def alias_info(ctx):
   140     """show the destination(s) of the specified alias"""
    74     """show the destination(s) of the specified alias"""
   141     if ctx.argc < 3:
    75     address = ctx.args.address.lower()
   142         usage(EX_MISSING_ARGS, _('Missing alias address.'), ctx.scmd)
       
   143     address = ctx.args[2].lower()
       
   144     try:
    76     try:
   145         _print_aliase_info(address, ctx.hdlr.alias_info(address))
    77         _print_aliase_info(address, ctx.hdlr.alias_info(address))
   146     except VMMError as err:
    78     except VMMError as err:
   147         if err.code is ACCOUNT_EXISTS:
    79         if err.code is ACCOUNT_EXISTS:
   148             w_err(0, ctx.plan_a_b % {'subcommand': 'userinfo',
    80             w_err(0, ctx.plan_a_b % {'subcommand': 'userinfo',
   149                   'object': address})
    81                   'object': address})
   150             ctx.scmd = ctx.args[1] = 'userinfo'
    82             ctx.args.scmd = 'userinfo'
       
    83             ctx.args.details = None
   151             user_info(ctx)
    84             user_info(ctx)
   152         elif err.code is RELOCATED_EXISTS:
    85         elif err.code is RELOCATED_EXISTS:
   153             w_err(0, ctx.plan_a_b % {'subcommand': 'relocatedinfo',
    86             w_err(0, ctx.plan_a_b % {'subcommand': 'relocatedinfo',
   154                   'object': address})
    87                   'object': address})
   155             ctx.scmd = ctx.args[1] = 'relocatedinfo'
    88             ctx.args.scmd = 'relocatedinfo'
   156             relocated_info(ctx)
    89             relocated_info(ctx)
   157         else:
    90         else:
   158             raise
    91             raise
   159 
    92 
   160 
    93 
   161 def aliasdomain_add(ctx):
    94 def aliasdomain_add(ctx):
   162     """create a new alias for an existing domain"""
    95     """create a new alias for an existing domain"""
   163     if ctx.argc < 3:
    96     ctx.hdlr.aliasdomain_add(ctx.args.fqdn.lower(),
   164         usage(EX_MISSING_ARGS, _('Missing alias domain name and destination '
    97                              ctx.args.destination.lower())
   165                                  'domain name.'), ctx.scmd)
       
   166     elif ctx.argc < 4:
       
   167         usage(EX_MISSING_ARGS, _('Missing destination domain name.'),
       
   168               ctx.scmd)
       
   169     ctx.hdlr.aliasdomain_add(ctx.args[2].lower(), ctx.args[3].lower())
       
   170 
    98 
   171 
    99 
   172 def aliasdomain_delete(ctx):
   100 def aliasdomain_delete(ctx):
   173     """delete the specified alias domain"""
   101     """delete the specified alias domain"""
   174     if ctx.argc < 3:
   102     ctx.hdlr.aliasdomain_delete(ctx.args.fqdn.lower())
   175         usage(EX_MISSING_ARGS, _('Missing alias domain name.'), ctx.scmd)
       
   176     ctx.hdlr.aliasdomain_delete(ctx.args[2].lower())
       
   177 
   103 
   178 
   104 
   179 def aliasdomain_info(ctx):
   105 def aliasdomain_info(ctx):
   180     """show the destination of the given alias domain"""
   106     """show the destination of the given alias domain"""
   181     if ctx.argc < 3:
   107     fqdn = ctx.args.fqdn.lower()
   182         usage(EX_MISSING_ARGS, _('Missing alias domain name.'), ctx.scmd)
       
   183     try:
   108     try:
   184         _print_aliasdomain_info(ctx.hdlr.aliasdomain_info(ctx.args[2].lower()))
   109         _print_aliasdomain_info(ctx.hdlr.aliasdomain_info(fqdn))
   185     except VMMError as err:
   110     except VMMError as err:
   186         if err.code is ALIASDOMAIN_ISDOMAIN:
   111         if err.code is ALIASDOMAIN_ISDOMAIN:
   187             w_err(0, ctx.plan_a_b % {'subcommand': 'domaininfo',
   112             w_err(0, ctx.plan_a_b % {'subcommand': 'domaininfo',
   188                   'object': ctx.args[2].lower()})
   113                                      'object': fqdn})
   189             ctx.scmd = ctx.args[1] = 'domaininfo'
   114             ctx.args.scmd = 'domaininfo'
   190             domain_info(ctx)
   115             domain_info(ctx)
   191         else:
   116         else:
   192             raise
   117             raise
   193 
   118 
   194 
   119 
   195 def aliasdomain_switch(ctx):
   120 def aliasdomain_switch(ctx):
   196     """assign the given alias domain to an other domain"""
   121     """assign the given alias domain to an other domain"""
   197     if ctx.argc < 3:
   122     ctx.hdlr.aliasdomain_switch(ctx.args.fqdn.lower(),
   198         usage(EX_MISSING_ARGS, _('Missing alias domain name and destination '
   123                                 ctx.args.destination.lower())
   199                                  'domain name.'), ctx.scmd)
       
   200     elif ctx.argc < 4:
       
   201         usage(EX_MISSING_ARGS, _('Missing destination domain name.'),
       
   202               ctx.scmd)
       
   203     ctx.hdlr.aliasdomain_switch(ctx.args[2].lower(), ctx.args[3].lower())
       
   204 
   124 
   205 
   125 
   206 def catchall_add(ctx):
   126 def catchall_add(ctx):
   207     """create a new catchall alias e-mail address"""
   127     """create a new catchall alias e-mail address"""
   208     if ctx.argc < 3:
   128     ctx.hdlr.catchall_add(ctx.args.fqdn.lower(), *ctx.args.destination)
   209         usage(EX_MISSING_ARGS, _('Missing domain and destination.'),
       
   210               ctx.scmd)
       
   211     elif ctx.argc < 4:
       
   212         usage(EX_MISSING_ARGS, _('Missing destination address.'), ctx.scmd)
       
   213     ctx.hdlr.catchall_add(ctx.args[2].lower(), *ctx.args[3:])
       
   214 
   129 
   215 
   130 
   216 def catchall_delete(ctx):
   131 def catchall_delete(ctx):
   217     """delete the specified destination or all of the catchall destination"""
   132     """delete the specified destination or all of the catchall destination"""
   218     if ctx.argc < 3:
   133     destination = ctx.args.destination if ctx.args.destination else None
   219         usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd)
   134     ctx.hdlr.catchall_delete(ctx.args.fqdn.lower(), destination)
   220     elif ctx.argc < 4:
       
   221         ctx.hdlr.catchall_delete(ctx.args[2].lower())
       
   222     else:
       
   223         ctx.hdlr.catchall_delete(ctx.args[2].lower(), ctx.args[3:])
       
   224 
   135 
   225 
   136 
   226 def catchall_info(ctx):
   137 def catchall_info(ctx):
   227     """show the catchall destination(s) of the specified domain"""
   138     """show the catchall destination(s) of the specified domain"""
   228     if ctx.argc < 3:
   139     address = ctx.args.fqdn.lower()
   229         usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd)
       
   230     address = ctx.args[2].lower()
       
   231     _print_catchall_info(address, ctx.hdlr.catchall_info(address))
   140     _print_catchall_info(address, ctx.hdlr.catchall_info(address))
   232 
   141 
   233 
   142 
   234 def config_get(ctx):
   143 def config_get(ctx):
   235     """show the actual value of the configuration option"""
   144     """show the actual value of the configuration option"""
   236     if ctx.argc < 3:
       
   237         usage(EX_MISSING_ARGS, _("Missing option name."), ctx.scmd)
       
   238 
       
   239     noop = lambda option: option
   145     noop = lambda option: option
   240     opt_formater = {
   146     opt_formater = {
   241         'misc.dovecot_version': version_str,
   147         'misc.dovecot_version': version_str,
   242         'domain.quota_bytes': human_size,
   148         'domain.quota_bytes': human_size,
   243     }
   149     }
   244 
   150 
   245     option = ctx.args[2].lower()
   151     option = ctx.args.option.lower()
   246     w_std('%s = %s' % (option, opt_formater.get(option,
   152     w_std('%s = %s' % (option, opt_formater.get(option,
   247                        noop)(ctx.cget(option))))
   153                        noop)(ctx.cget(option))))
   248 
   154 
   249 
   155 
   250 def config_set(ctx):
   156 def config_set(ctx):
   251     """set a new value for the configuration option"""
   157     """set a new value for the configuration option"""
   252     if ctx.argc < 3:
   158     ctx.hdlr.cfg_set(ctx.args.option.lower(), ctx.args.value)
   253         usage(EX_MISSING_ARGS, _('Missing option and new value.'), ctx.scmd)
       
   254     if ctx.argc < 4:
       
   255         usage(EX_MISSING_ARGS, _('Missing new configuration value.'),
       
   256               ctx.scmd)
       
   257     ctx.hdlr.cfg_set(ctx.args[2].lower(), ctx.args[3])
       
   258 
   159 
   259 
   160 
   260 def configure(ctx):
   161 def configure(ctx):
   261     """start interactive configuration mode"""
   162     """start interactive configuration mode"""
   262     if ctx.argc < 3:
   163     ctx.hdlr.configure(ctx.args.section)
   263         ctx.hdlr.configure()
       
   264     else:
       
   265         ctx.hdlr.configure(ctx.args[2].lower())
       
   266 
   164 
   267 
   165 
   268 def domain_add(ctx):
   166 def domain_add(ctx):
   269     """create a new domain"""
   167     """create a new domain"""
   270     if ctx.argc < 3:
   168     fqdn = ctx.args.fqdn.lower()
   271         usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd)
   169     transport = ctx.args.transport.lower if ctx.args.transport else None
   272     elif ctx.argc < 4:
   170     ctx.hdlr.domain_add(fqdn, transport)
   273         ctx.hdlr.domain_add(ctx.args[2].lower())
       
   274     else:
       
   275         ctx.hdlr.domain_add(ctx.args[2].lower(), ctx.args[3])
       
   276     if ctx.cget('domain.auto_postmaster'):
   171     if ctx.cget('domain.auto_postmaster'):
   277         w_std(_('Creating account for postmaster@%s') % ctx.args[2].lower())
   172         w_std(_('Creating account for postmaster@%s') % fqdn)
   278         ctx.scmd = 'useradd'
   173         ctx.args.scmd = 'useradd'
   279         ctx.args = [prog, ctx.scmd, 'postmaster@' + ctx.args[2].lower()]
   174         ctx.args.address = 'postmaster@%s' % fqdn
   280         ctx.argc = 3
   175         ctx.args.password = None
   281         user_add(ctx)
   176         user_add(ctx)
   282 
   177 
   283 
   178 
   284 def domain_delete(ctx):
   179 def domain_delete(ctx):
   285     """delete the given domain and all its alias domains"""
   180     """delete the given domain and all its alias domains"""
   286     if ctx.argc < 3:
   181     ctx.hdlr.domain_delete(ctx.args.fqdn.lower(), ctx.args.force)
   287         usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd)
       
   288     elif ctx.argc < 4:
       
   289         ctx.hdlr.domain_delete(ctx.args[2].lower())
       
   290     elif ctx.args[3].lower() == 'force':
       
   291         ctx.hdlr.domain_delete(ctx.args[2].lower(), True)
       
   292     else:
       
   293         usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % ctx.args[3],
       
   294               ctx.scmd)
       
   295 
   182 
   296 
   183 
   297 def domain_info(ctx):
   184 def domain_info(ctx):
   298     """display information about the given domain"""
   185     """display information about the given domain"""
   299     if ctx.argc < 3:
   186     fqdn = ctx.args.fqdn.lower()
   300         usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd)
   187     details = ctx.args.details
   301     if ctx.argc < 4:
       
   302         details = None
       
   303     else:
       
   304         details = ctx.args[3].lower()
       
   305         if details not in ('accounts', 'aliasdomains', 'aliases', 'full',
       
   306                            'relocated', 'catchall'):
       
   307             usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % details,
       
   308                   ctx.scmd)
       
   309     try:
   188     try:
   310         info = ctx.hdlr.domain_info(ctx.args[2].lower(), details)
   189         info = ctx.hdlr.domain_info(fqdn, details)
   311     except VMMError as err:
   190     except VMMError as err:
   312         if err.code is DOMAIN_ALIAS_EXISTS:
   191         if err.code is DOMAIN_ALIAS_EXISTS:
   313             w_err(0, ctx.plan_a_b % {'subcommand': 'aliasdomaininfo',
   192             w_err(0, ctx.plan_a_b % {'subcommand': 'aliasdomaininfo',
   314                   'object': ctx.args[2].lower()})
   193                                      'object': fqdn})
   315             ctx.scmd = ctx.args[1] = 'aliasdomaininfo'
   194             ctx.args.scmd = 'aliasdomaininfo'
   316             aliasdomain_info(ctx)
   195             aliasdomain_info(ctx)
   317         else:
   196         else:
   318             raise
   197             raise
   319     else:
   198     else:
   320         q_limit = 'Storage: %(bytes)s; Messages: %(messages)s'
   199         q_limit = 'Storage: %(bytes)s; Messages: %(messages)s'
   347                 _print_list(info[5], _('catch-all destinations'))
   226                 _print_list(info[5], _('catch-all destinations'))
   348 
   227 
   349 
   228 
   350 def domain_quota(ctx):
   229 def domain_quota(ctx):
   351     """update the quota limit of the specified domain"""
   230     """update the quota limit of the specified domain"""
   352     if ctx.argc < 3:
   231     force = 'force' if ctx.args.force else None
   353         usage(EX_MISSING_ARGS, _('Missing domain name and storage value.'),
   232     ctx.hdlr.domain_quotalimit(ctx.args.fqdn.lower(), ctx.args.storage,
   354               ctx.scmd)
   233                                ctx.args.messages, force)
   355     if ctx.argc < 4:
       
   356         usage(EX_MISSING_ARGS, _('Missing storage value.'), ctx.scmd)
       
   357     messages = 0
       
   358     force = None
       
   359     try:
       
   360         bytes_ = size_in_bytes(ctx.args[3])
       
   361     except (ValueError, TypeError):
       
   362         usage(INVALID_ARGUMENT, _("Invalid storage value: '%s'") %
       
   363               ctx.args[3], ctx.scmd)
       
   364     if ctx.argc < 5:
       
   365         pass
       
   366     elif ctx.argc < 6:
       
   367         try:
       
   368             messages = int(ctx.args[4])
       
   369         except ValueError:
       
   370             if ctx.args[4].lower() != 'force':
       
   371                 usage(INVALID_ARGUMENT,
       
   372                       _("Neither a valid number of messages nor the keyword "
       
   373                         "'force': '%s'") % ctx.args[4], ctx.scmd)
       
   374             force = 'force'
       
   375     else:
       
   376         try:
       
   377             messages = int(ctx.args[4])
       
   378         except ValueError:
       
   379             usage(INVALID_ARGUMENT,
       
   380                   _("Not a valid number of messages: '%s'") % ctx.args[4],
       
   381                   ctx.scmd)
       
   382         if ctx.args[5].lower() != 'force':
       
   383             usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % ctx.args[5],
       
   384                   ctx.scmd)
       
   385         force = 'force'
       
   386     ctx.hdlr.domain_quotalimit(ctx.args[2].lower(), bytes_, messages, force)
       
   387 
   234 
   388 
   235 
   389 def domain_services(ctx):
   236 def domain_services(ctx):
   390     """allow all named service and block the uncredited."""
   237     """allow all named service and block the uncredited."""
   391     if ctx.argc < 3:
   238     force = 'force' if ctx.args.force else None
   392         usage(EX_MISSING_ARGS, _('Missing domain name.'), ctx.scmd)
   239     services = ctx.args.services if ctx.args.services else []
   393     services = []
   240     ctx.hdlr.domain_services(ctx.args.fqdn.lower(), force, *services)
   394     force = False
       
   395     if ctx.argc is 3:
       
   396         pass
       
   397     elif ctx.argc is 4:
       
   398         arg = ctx.args[3].lower()
       
   399         if arg in SERVICES:
       
   400             services.append(arg)
       
   401         elif arg == 'force':
       
   402             force = True
       
   403         else:
       
   404             usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % arg,
       
   405                   ctx.scmd)
       
   406     else:
       
   407         services.extend([service.lower() for service in ctx.args[3:-1]])
       
   408         arg = ctx.args[-1].lower()
       
   409         if arg == 'force':
       
   410             force = True
       
   411         else:
       
   412             services.append(arg)
       
   413         unknown = [service for service in services if service not in SERVICES]
       
   414         if unknown:
       
   415             usage(INVALID_ARGUMENT, _('Invalid service arguments: %s') %
       
   416                   ' '.join(unknown), ctx.scmd)
       
   417     ctx.hdlr.domain_services(ctx.args[2].lower(), (None, 'force')[force],
       
   418                              *services)
       
   419 
   241 
   420 
   242 
   421 def domain_transport(ctx):
   243 def domain_transport(ctx):
   422     """update the transport of the specified domain"""
   244     """update the transport of the specified domain"""
   423     if ctx.argc < 3:
   245     force = 'force' if ctx.args.force else None
   424         usage(EX_MISSING_ARGS, _('Missing domain name and new transport.'),
   246     ctx.hdlr.domain_transport(ctx.args.fqdn.lower(),
   425               ctx.scmd)
   247                               ctx.args.transport.lower(), force)
   426     if ctx.argc < 4:
       
   427         usage(EX_MISSING_ARGS, _('Missing new transport.'), ctx.scmd)
       
   428     if ctx.argc < 5:
       
   429         ctx.hdlr.domain_transport(ctx.args[2].lower(), ctx.args[3])
       
   430     else:
       
   431         force = ctx.args[4].lower()
       
   432         if force != 'force':
       
   433             usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % force,
       
   434                   ctx.scmd)
       
   435         ctx.hdlr.domain_transport(ctx.args[2].lower(), ctx.args[3], force)
       
   436 
   248 
   437 
   249 
   438 def domain_note(ctx):
   250 def domain_note(ctx):
   439     """update the note of the given domain"""
   251     """update the note of the given domain"""
   440     if ctx.argc < 3:
   252     ctx.hdlr.domain_note(ctx.args.fqdn.lower(), ctx.args.note)
   441         usage(EX_MISSING_ARGS, _('Missing domain name.'),
       
   442               ctx.scmd)
       
   443     elif ctx.argc < 4:
       
   444         note = None
       
   445     else:
       
   446         note = ' '.join(ctx.args[3:])
       
   447     ctx.hdlr.domain_note(ctx.args[2].lower(), note)
       
   448 
   253 
   449 
   254 
   450 def get_user(ctx):
   255 def get_user(ctx):
   451     """get the address of the user with the given UID"""
   256     """get the address of the user with the given UID"""
   452     if ctx.argc < 3:
   257     _print_info(ctx, ctx.hdlr.user_by_uid(ctx.args.uid), _('Account'))
   453         usage(EX_MISSING_ARGS, _('Missing UID.'), ctx.scmd)
       
   454     _print_info(ctx, ctx.hdlr.user_by_uid(ctx.args[2]), _('Account'))
       
   455 
       
   456 
       
   457 def help_(ctx):
       
   458     """print help messages."""
       
   459     if ctx.argc > 2:
       
   460         hlptpc = ctx.args[2].lower()
       
   461         if hlptpc in cmd_map:
       
   462             topic = hlptpc
       
   463         else:
       
   464             for scmd in cmd_map.values():
       
   465                 if scmd.alias == hlptpc:
       
   466                     topic = scmd.name
       
   467                     break
       
   468             else:
       
   469                 usage(INVALID_ARGUMENT, _("Unknown help topic: '%s'") %
       
   470                       ctx.args[2], ctx.scmd)
       
   471         if topic != 'help':
       
   472             return cmd_map[topic].help_()
       
   473 
       
   474     old_ii = txt_wrpr.initial_indent
       
   475     old_si = txt_wrpr.subsequent_indent
       
   476     txt_wrpr.initial_indent = ' '
       
   477     # len(max(_overview.iterkeys(), key=len)) #Py25
       
   478     txt_wrpr.subsequent_indent = 20 * ' '
       
   479     order = sorted(list(cmd_map.keys()))
       
   480 
       
   481     w_std(_('List of available subcommands:') + '\n')
       
   482     for key in order:
       
   483         w_std('\n'.join(txt_wrpr.wrap('%-18s %s' % (key, cmd_map[key].descr))))
       
   484 
       
   485     txt_wrpr.initial_indent = old_ii
       
   486     txt_wrpr.subsequent_indent = old_si
       
   487     txt_wrpr.initial_indent = ''
       
   488 
   258 
   489 
   259 
   490 def list_domains(ctx):
   260 def list_domains(ctx):
   491     """list all domains / search domains by pattern"""
   261     """list all domains / search domains by pattern"""
   492     matching = ctx.argc > 2
   262     matching = True if ctx.args.pattern else False
   493     if matching:
   263     pattern = ctx.args.pattern.lower() if matching else None
   494         gids, domains = ctx.hdlr.domain_list(ctx.args[2].lower())
   264     gids, domains = ctx.hdlr.domain_list(pattern)
   495     else:
       
   496         gids, domains = ctx.hdlr.domain_list()
       
   497     _print_domain_list(gids, domains, matching)
   265     _print_domain_list(gids, domains, matching)
   498 
   266 
   499 
   267 
   500 def list_pwschemes(ctx_unused):
   268 def list_pwschemes(ctx_unused):
   501     """Prints all usable password schemes and password encoding suffixes."""
   269     """Prints all usable password schemes and password encoding suffixes."""
   517     limited with TYPE_ACCOUNT, TYPE_ALIAS and TYPE_RELOCATED, which can be
   285     limited with TYPE_ACCOUNT, TYPE_ALIAS and TYPE_RELOCATED, which can be
   518     bitwise ORed as a combination. Not specifying a limit is the same as
   286     bitwise ORed as a combination. Not specifying a limit is the same as
   519     combining all three."""
   287     combining all three."""
   520     if limit is None:
   288     if limit is None:
   521         limit = TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED
   289         limit = TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED
   522     matching = ctx.argc > 2
   290     matching = True if ctx.args.pattern else False
   523     if matching:
   291     pattern = ctx.args.pattern.lower() if matching else None
   524         gids, addresses = ctx.hdlr.address_list(limit, ctx.args[2].lower())
   292     gids, addresses = ctx.hdlr.address_list(limit, pattern)
   525     else:
       
   526         gids, addresses = ctx.hdlr.address_list(limit)
       
   527     _print_address_list(limit, gids, addresses, matching)
   293     _print_address_list(limit, gids, addresses, matching)
   528 
   294 
   529 
   295 
   530 def list_users(ctx):
   296 def list_users(ctx):
   531     """list all user accounts / search user accounts by pattern"""
   297     """list all user accounts / search user accounts by pattern"""
   542     return list_addresses(ctx, TYPE_RELOCATED)
   308     return list_addresses(ctx, TYPE_RELOCATED)
   543 
   309 
   544 
   310 
   545 def relocated_add(ctx):
   311 def relocated_add(ctx):
   546     """create a new record for a relocated user"""
   312     """create a new record for a relocated user"""
   547     if ctx.argc < 3:
   313     ctx.hdlr.relocated_add(ctx.args.address.lower(), ctx.args.newaddress)
   548         usage(EX_MISSING_ARGS,
       
   549               _('Missing relocated address and destination.'), ctx.scmd)
       
   550     elif ctx.argc < 4:
       
   551         usage(EX_MISSING_ARGS, _('Missing destination address.'), ctx.scmd)
       
   552     ctx.hdlr.relocated_add(ctx.args[2].lower(), ctx.args[3])
       
   553 
   314 
   554 
   315 
   555 def relocated_delete(ctx):
   316 def relocated_delete(ctx):
   556     """delete the record of the relocated user"""
   317     """delete the record of the relocated user"""
   557     if ctx.argc < 3:
   318     ctx.hdlr.relocated_delete(ctx.args.address.lower())
   558         usage(EX_MISSING_ARGS, _('Missing relocated address.'), ctx.scmd)
       
   559     ctx.hdlr.relocated_delete(ctx.args[2].lower())
       
   560 
   319 
   561 
   320 
   562 def relocated_info(ctx):
   321 def relocated_info(ctx):
   563     """print information about a relocated user"""
   322     """print information about a relocated user"""
   564     if ctx.argc < 3:
   323     relocated = ctx.args.address.lower()
   565         usage(EX_MISSING_ARGS, _('Missing relocated address.'), ctx.scmd)
       
   566     relocated = ctx.args[2].lower()
       
   567     try:
   324     try:
   568         _print_relocated_info(addr=relocated,
   325         _print_relocated_info(addr=relocated,
   569                               dest=ctx.hdlr.relocated_info(relocated))
   326                               dest=ctx.hdlr.relocated_info(relocated))
   570     except VMMError as err:
   327     except VMMError as err:
   571         if err.code is ACCOUNT_EXISTS:
   328         if err.code is ACCOUNT_EXISTS:
   572             w_err(0, ctx.plan_a_b % {'subcommand': 'userinfo',
   329             w_err(0, ctx.plan_a_b % {'subcommand': 'userinfo',
   573                   'object': relocated})
   330                   'object': relocated})
   574             ctx.scmd = ctx.args[1] = 'userinfoi'
   331             ctx.args.scmd = 'userinfo'
       
   332             ctx.args.details = None
   575             user_info(ctx)
   333             user_info(ctx)
   576         elif err.code is ALIAS_EXISTS:
   334         elif err.code is ALIAS_EXISTS:
   577             w_err(0, ctx.plan_a_b % {'subcommand': 'aliasinfo',
   335             w_err(0, ctx.plan_a_b % {'subcommand': 'aliasinfo',
   578                   'object': relocated})
   336                   'object': relocated})
   579             ctx.scmd = ctx.args[1] = 'aliasinfo'
   337             ctx.args.scmd = 'aliasinfo'
   580             alias_info(ctx)
   338             alias_info(ctx)
   581         else:
   339         else:
   582             raise
   340             raise
   583 
   341 
   584 
   342 
   585 def user_add(ctx):
   343 def user_add(ctx):
   586     """create a new e-mail user with the given address"""
   344     """create a new e-mail user with the given address"""
   587     if ctx.argc < 3:
   345     gen_pass = ctx.hdlr.user_add(ctx.args.address.lower(), ctx.args.password)
   588         usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd)
   346     if not ctx.args.password and gen_pass:
   589     elif ctx.argc < 4:
       
   590         password = None
       
   591     else:
       
   592         password = ctx.args[3]
       
   593     gen_pass = ctx.hdlr.user_add(ctx.args[2].lower(), password)
       
   594     if ctx.argc < 4 and gen_pass:
       
   595         w_std(_("Generated password: %s") % gen_pass)
   347         w_std(_("Generated password: %s") % gen_pass)
   596 
   348 
   597 
   349 
   598 def user_delete(ctx):
   350 def user_delete(ctx):
   599     """delete the specified user"""
   351     """delete the specified user"""
   600     if ctx.argc < 3:
   352     ctx.hdlr.user_delete(ctx.args.address.lower(), ctx.args.force)
   601         usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd)
       
   602     elif ctx.argc < 4:
       
   603         ctx.hdlr.user_delete(ctx.args[2].lower())
       
   604     elif ctx.args[3].lower() == 'force':
       
   605         ctx.hdlr.user_delete(ctx.args[2].lower(), True)
       
   606     else:
       
   607         usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % ctx.args[3],
       
   608               ctx.scmd)
       
   609 
   353 
   610 
   354 
   611 def user_info(ctx):
   355 def user_info(ctx):
   612     """display information about the given address"""
   356     """display information about the given address"""
   613     if ctx.argc < 3:
   357     address = ctx.args.address.lower()
   614         usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd)
       
   615     if ctx.argc < 4:
       
   616         details = None
       
   617     else:
       
   618         details = ctx.args[3].lower()
       
   619         if details not in ('aliases', 'du', 'full'):
       
   620             usage(INVALID_ARGUMENT, _("Invalid argument: '%s'") % details,
       
   621                   ctx.scmd)
       
   622     try:
   358     try:
   623         info = ctx.hdlr.user_info(ctx.args[2].lower(), details)
   359         info = ctx.hdlr.user_info(address, ctx.args.details)
   624     except VMMError as err:
   360     except VMMError as err:
   625         if err.code is ALIAS_EXISTS:
   361         if err.code is ALIAS_EXISTS:
   626             w_err(0, ctx.plan_a_b % {'subcommand': 'aliasinfo',
   362             w_err(0, ctx.plan_a_b % {'subcommand': 'aliasinfo',
   627                   'object': ctx.args[2].lower()})
   363                   'object': address})
   628             ctx.scmd = ctx.args[1] = 'aliasinfo'
   364             ctx.args.scmd = 'aliasinfo'
   629             alias_info(ctx)
   365             alias_info(ctx)
   630         elif err.code is RELOCATED_EXISTS:
   366         elif err.code is RELOCATED_EXISTS:
   631             w_err(0, ctx.plan_a_b % {'subcommand': 'relocatedinfo',
   367             w_err(0, ctx.plan_a_b % {'subcommand': 'relocatedinfo',
   632                   'object': ctx.args[2].lower()})
   368                   'object': address})
   633             ctx.scmd = ctx.args[1] = 'relocatedinfo'
   369             ctx.args.scmd = 'relocatedinfo'
   634             relocated_info(ctx)
   370             relocated_info(ctx)
   635         else:
   371         else:
   636             raise
   372             raise
   637     else:
   373     else:
   638         if details in (None, 'du'):
   374         if ctx.args.details in (None, 'du'):
   639             info['quota storage'] = _format_quota_usage(info['ql_bytes'],
   375             info['quota storage'] = _format_quota_usage(info['ql_bytes'],
   640                     info['uq_bytes'], True, info['ql_domaindefault'])
   376                     info['uq_bytes'], True, info['ql_domaindefault'])
   641             info['quota messages'] = \
   377             info['quota messages'] = \
   642                 _format_quota_usage(info['ql_messages'],
   378                 _format_quota_usage(info['ql_messages'],
   643                                     info['uq_messages'],
   379                                     info['uq_messages'],
   654             _print_list(info[1], _('alias addresses'))
   390             _print_list(info[1], _('alias addresses'))
   655 
   391 
   656 
   392 
   657 def user_name(ctx):
   393 def user_name(ctx):
   658     """set or update the real name for an address"""
   394     """set or update the real name for an address"""
   659     if ctx.argc < 3:
   395     ctx.hdlr.user_name(ctx.args.address.lower(), ctx.args.name)
   660         usage(EX_MISSING_ARGS, _("Missing e-mail address and user's name."),
       
   661               ctx.scmd)
       
   662     elif ctx.argc < 4:
       
   663         name = None
       
   664     else:
       
   665         name = ctx.args[3]
       
   666     ctx.hdlr.user_name(ctx.args[2].lower(), name)
       
   667 
   396 
   668 
   397 
   669 def user_password(ctx):
   398 def user_password(ctx):
   670     """update the password for the given address"""
   399     """update the password for the given address"""
   671     if ctx.argc < 3:
   400     ctx.hdlr.user_password(ctx.args.address.lower(), ctx.args.password)
   672         usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd)
       
   673     elif ctx.argc < 4:
       
   674         password = None
       
   675     else:
       
   676         password = ctx.args[3]
       
   677     ctx.hdlr.user_password(ctx.args[2].lower(), password)
       
   678 
   401 
   679 
   402 
   680 def user_note(ctx):
   403 def user_note(ctx):
   681     """update the note of the given address"""
   404     """update the note of the given address"""
   682     if ctx.argc < 3:
   405     ctx.hdlr.user_note(ctx.args.address.lower(), ctx.args.note)
   683         usage(EX_MISSING_ARGS, _('Missing e-mail address.'),
       
   684               ctx.scmd)
       
   685     elif ctx.argc < 4:
       
   686         note = None
       
   687     else:
       
   688         note = ' '.join(ctx.args[3:])
       
   689     ctx.hdlr.user_note(ctx.args[2].lower(), note)
       
   690 
   406 
   691 
   407 
   692 def user_quota(ctx):
   408 def user_quota(ctx):
   693     """update the quota limit for the given address"""
   409     """update the quota limit for the given address"""
   694     if ctx.argc < 3:
   410     ctx.hdlr.user_quotalimit(ctx.args.address.lower(), ctx.args.storage,
   695         usage(EX_MISSING_ARGS, _('Missing e-mail address and storage value.'),
   411                              ctx.args.messages)
   696               ctx.scmd)
       
   697     elif ctx.argc < 4:
       
   698         usage(EX_MISSING_ARGS, _('Missing storage value.'), ctx.scmd)
       
   699     if ctx.args[3] != 'domain':
       
   700         try:
       
   701             bytes_ = size_in_bytes(ctx.args[3])
       
   702         except (ValueError, TypeError):
       
   703             usage(INVALID_ARGUMENT, _("Invalid storage value: '%s'") %
       
   704                   ctx.args[3], ctx.scmd)
       
   705     else:
       
   706         bytes_ = ctx.args[3]
       
   707     if ctx.argc < 5:
       
   708         messages = 0
       
   709     else:
       
   710         try:
       
   711             messages = int(ctx.args[4])
       
   712         except ValueError:
       
   713             usage(INVALID_ARGUMENT,
       
   714                   _("Not a valid number of messages: '%s'") % ctx.args[4],
       
   715                   ctx.scmd)
       
   716     ctx.hdlr.user_quotalimit(ctx.args[2].lower(), bytes_, messages)
       
   717 
   412 
   718 
   413 
   719 def user_services(ctx):
   414 def user_services(ctx):
   720     """allow all named service and block the uncredited."""
   415     """allow all named service and block the uncredited."""
   721     if ctx.argc < 3:
   416     if 'domain' in ctx.args.services:
   722         usage(EX_MISSING_ARGS, _('Missing e-mail address.'), ctx.scmd)
   417         services = ['domain']
   723     services = []
   418     else:
   724     if ctx.argc >= 4:
   419         services = ctx.args.services
   725         services.extend([service.lower() for service in ctx.args[3:]])
   420     ctx.hdlr.user_services(ctx.args.address.lower(), *services)
   726         unknown = [service for service in services if service not in SERVICES]
       
   727         if unknown and ctx.args[3] != 'domain':
       
   728             usage(INVALID_ARGUMENT, _('Invalid service arguments: %s') %
       
   729                   ' '.join(unknown), ctx.scmd)
       
   730     ctx.hdlr.user_services(ctx.args[2].lower(), *services)
       
   731 
   421 
   732 
   422 
   733 def user_transport(ctx):
   423 def user_transport(ctx):
   734     """update the transport of the given address"""
   424     """update the transport of the given address"""
   735     if ctx.argc < 3:
   425     ctx.hdlr.user_transport(ctx.args.address.lower(), ctx.args.transport)
   736         usage(EX_MISSING_ARGS, _('Missing e-mail address and transport.'),
   426 
   737               ctx.scmd)
   427 
   738     if ctx.argc < 4:
   428 def setup_parser():
   739         usage(EX_MISSING_ARGS, _('Missing transport.'), ctx.scmd)
   429     """Create the argument parser, add all the subcommands and return it."""
   740     ctx.hdlr.user_transport(ctx.args[2].lower(), ctx.args[3])
   430     class ArgParser(ArgumentParser):
   741 
   431         """This class fixes the 'width detection'."""
   742 
   432         def _get_formatter(self):
   743 def usage(errno, errmsg, subcommand=None):
   433             return self.formatter_class(prog=self.prog, width=WS_ROWS,
   744     """print usage message for the given command or all commands.
   434                                         max_help_position=26)
   745     When errno > 0, sys,exit(errno) will interrupt the program.
   435 
   746     """
   436     class VersionAction(Action):
   747     if subcommand and subcommand in cmd_map:
   437         """Show version and copyright information."""
   748         w_err(errno, _("Error: %s") % errmsg,
   438         def __call__(self, parser, namespace, values, option_string=None):
   749               _("usage: ") + cmd_map[subcommand].usage)
   439             """implements the Action API."""
   750 
   440             vers_info = _('{program}, version {version} (from {rel_date})\n'
   751     # TP: Please adjust translated words like the original text.
   441                           'Python {py_vers} on {sysname}'.format(
   752     # (It's a table header.) Extract from usage text:
   442                               program=parser.prog, version=__version__,
   753     # usage: vmm subcommand arguments
   443                               rel_date=strftime(
   754     #   short long
   444                                             locale.nl_langinfo(locale.D_FMT),
   755     #   subcommand                arguments
   445                                             strptime(__date__, '%Y-%m-%d')),
   756     #
   446                               py_vers=platform.python_version(),
   757     #   da    domainadd           fqdn [transport]
   447                               sysname=platform.system()))
   758     #   dd    domaindelete        fqdn [force]
   448             copy_info = _('{copyright}\n{program} is free software and comes '
   759     u_head = _("""usage: %s subcommand arguments
   449                           'with ABSOLUTELY NO WARRANTY.'.format(
   760   short long
   450                               copyright=__copyright__, program=parser.prog))
   761   subcommand                arguments\n""") % prog
   451             parser.exit(message='\n\n'.join((vers_info, copy_info)) + '\n')
   762     order = sorted(list(cmd_map.keys()))
   452 
   763     w_err(0, u_head)
   453     def quota_storage(string):
   764     for key in order:
   454         if string == 'domain':
   765         scmd = cmd_map[key]
   455             return string
   766         w_err(0, '  %-5s %-19s %s' % (scmd.alias, scmd.name, scmd.args))
   456         try:
   767     w_err(errno, '', _("Error: %s") % errmsg)
   457             storage = size_in_bytes(string)
   768 
   458         except (TypeError, ValueError) as error:
   769 
   459             raise ArgumentTypeError(str(error))
   770 def version(ctx_unused):
   460         return storage
   771     """Write version and copyright information to stdout."""
   461 
   772     w_std('%s, %s %s (%s %s)\nPython %s %s %s\n\n%s\n%s %s' % (prog,
   462     old_rw = txt_wrpr.replace_whitespace
   773     # TP: The words 'from', 'version' and 'on' are used in
   463     txt_wrpr.replace_whitespace = False
   774     # the version information, e.g.:
   464     fill = lambda t: '\n'.join(txt_wrpr.fill(l) for l in t.splitlines(True))
   775     # vmm, version 0.5.2 (from 09/09/09)
   465     mklst = lambda iterable: '\n\t - ' + '\n\t - '.join(iterable)
   776     # Python 2.5.4 on FreeBSD
   466 
   777         _('version'), __version__, _('from'),
   467     description = _('%(prog)s - command line tool to manage email '
   778         strftime(locale.nl_langinfo(locale.D_FMT),
   468                     'domains/accounts/aliases/...')
   779             strptime(__date__, '%Y-%m-%d')),
   469     epilog = _('use "%(prog)s <subcommand> -h" for information about the '
   780         os.sys.version.split()[0], _('on'), os.uname()[0],
   470                'given subcommand')
   781         __copyright__, prog,
   471     parser = ArgParser(description=description, epilog=epilog)
   782         _('is free software and comes with ABSOLUTELY NO WARRANTY.')))
   472     parser.add_argument('-v', '--version', action=VersionAction, nargs=0,
   783 
   473                         help=_("show %(prog)s's version and copyright "
   784 
   474                                "information and exit"))
   785 def update_cmd_map():
   475     subparsers = parser.add_subparsers(metavar=_('<subcommand>'),
   786     """Update the cmd_map, after gettext's _ was installed."""
   476                                      title=_('list of available subcommands'))
   787     cmd = Command
   477     a = subparsers.add_parser
   788     cmd_map.update({
   478 
   789     # Account commands
   479     ###
   790     'getuser': cmd('getuser', 'gu', get_user, 'uid',
   480     # general subcommands
   791                    _('get the address of the user with the given UID')),
   481     ###
   792     'useradd': cmd('useradd', 'ua', user_add, 'address [password]',
   482     cg = a('configget', aliases=('cg',),
   793                    _('create a new e-mail user with the given address')),
   483            help=_('show the actual value of the configuration option'),
   794     'userdelete': cmd('userdelete', 'ud', user_delete, 'address [force]',
   484            epilog=_("This subcommand is used to display the actual value of "
   795                       _('delete the specified user')),
   485            "the given configuration option."))
   796     'userinfo': cmd('userinfo', 'ui', user_info, 'address [details]',
   486     cg.add_argument('option', help=_('the name of a configuration option'))
   797                     _('display information about the given address')),
   487     cg.set_defaults(func=config_get, scmd='configget')
   798     'username': cmd('username', 'un', user_name, 'address [name]',
   488 
   799                     _('set, update or delete the real name for an address')),
   489     cs = a('configset', aliases=('cs',),
   800     'userpassword': cmd('userpassword', 'up', user_password,
   490            help=_('set a new value for the configuration option'),
   801                         'address [password]',
   491            epilog=fill(_("Use this subcommand to set or update a single "
   802                         _('update the password for the given address')),
   492                "configuration option's value. option is the configuration "
   803     'userquota': cmd('userquota', 'uq', user_quota,
   493                "option, value is the option's new value.\n\nNote: This "
   804                      'address storage [messages] | address domain',
   494                "subcommand will create a new vmm.cfg without any comments. "
   805                      _('update the quota limit for the given address')),
   495                "Your current configuration file will be backed as "
   806     'userservices': cmd('userservices', 'us', user_services,
   496                "vmm.cfg.bak.")),
   807                         'address [service ...] | address domain',
   497            formatter_class=RawDescriptionHelpFormatter)
   808                         _('enables the specified services and disables all '
   498     cs.add_argument('option', help=_('the name of a configuration option'))
   809                           'not specified services')),
   499     cs.add_argument('value', help=_("the option's new value"))
   810     'usertransport': cmd('usertransport', 'ut', user_transport,
   500     cs.set_defaults(func=config_set, scmd='configset')
   811                          'address transport | address domain',
   501 
   812                          _('update the transport of the given address')),
   502     sections = ('account', 'bin', 'database', 'domain', 'mailbox', 'misc')
   813     'usernote': cmd('usernote', 'uo', user_note, 'address [note]',
   503     cf = a('configure', aliases=('cf',),
   814                     _('set, update or delete the note of the given address')),
   504            help=_('start interactive configuration mode'),
   815     # Alias commands
   505            epilog=fill(_("Starts the interactive configuration for all "
   816     'aliasadd': cmd('aliasadd', 'aa', alias_add, 'address destination ...',
   506                "configuration sections.\n\nIn this process the currently set "
   817                     _('create a new alias e-mail address with one or more '
   507                "value of each option will be displayed in square brackets. "
   818                       'destinations')),
   508                "If no value is configured, the default value of each option "
   819     'aliasdelete': cmd('aliasdelete', 'ad', alias_delete,
   509                "will be displayed in square brackets. Press the return key, "
   820                        'address [destination ...]',
   510                "to accept the displayed value.\n\n"
   821                        _('delete the specified alias e-mail address or one '
   511                "If the optional argument section is given, only the "
   822                          'of its destinations')),
   512                "configuration options from the given section will be "
   823     'aliasinfo': cmd('aliasinfo', 'ai', alias_info, 'address',
   513                "displayed and will be configurable. The following sections "
   824                      _('show the destination(s) of the specified alias')),
   514                "are available:\n") + mklst(sections)),
   825     # AliasDomain commands
   515            formatter_class=RawDescriptionHelpFormatter)
   826     'aliasdomainadd': cmd('aliasdomainadd', 'ada', aliasdomain_add,
   516     cf.add_argument('-s', choices=sections, metavar='SECTION', dest='section',
   827                           'fqdn destination',
   517                     help=_("configure only options of the given section"))
   828                           _('create a new alias for an existing domain')),
   518     cf.set_defaults(func=configure, scmd='configure')
   829     'aliasdomaindelete': cmd('aliasdomaindelete', 'add', aliasdomain_delete,
   519 
   830                              'fqdn', _('delete the specified alias domain')),
   520     gu = a('getuser', aliases=('gu',),
   831     'aliasdomaininfo': cmd('aliasdomaininfo', 'adi', aliasdomain_info, 'fqdn',
   521            help=_('get the address of the user with the given UID'),
   832                          _('show the destination of the given alias domain')),
   522            epilog=_("If only the uid is available, for example from process "
   833     'aliasdomainswitch': cmd('aliasdomainswitch', 'ads', aliasdomain_switch,
   523                     "list, the subcommand getuser will show the user's "
   834                              'fqdn destination', _('assign the given alias '
   524                     "address."))
   835                              'domain to an other domain')),
   525     gu.add_argument('uid', type=int, help=_("a user's unique identifier"))
   836     # CatchallAlias commands
   526     gu.set_defaults(func=get_user, scmd='getuser')
   837     'catchalladd': cmd('catchalladd', 'caa', catchall_add,
   527 
   838                        'fqdn destination ...',
   528     ll = a('listaddresses', aliases=('ll',),
   839                        _('add one or more catch-all destinations for a '
   529            help=_('list all addresses or search for addresses by pattern'),
   840                          'domain')),
   530            epilog=fill(_("This command lists all defined addresses. "
   841     'catchalldelete': cmd('catchalldelete', 'cad', catchall_delete,
   531                "Addresses belonging to alias-domains are prefixed with a '-', "
   842                        'fqdn [destination ...]',
   532                "addresses of regular domains with a '+'. Additionally, the "
   843                        _('delete the specified catch-all destination or all '
   533                "letters 'u', 'a', and 'r' indicate the type of each address: "
   844                          'of a domain\'s destinations')),
   534                "user, alias and relocated respectively. The output can be "
   845     'catchallinfo': cmd('catchallinfo', 'cai', catchall_info, 'fqdn',
   535                "limited with an optional pattern.\n\nTo perform a wild card "
   846                         _('show the catch-all destination(s) of the '
   536                "search, the % character can be used at the start and/or the "
   847                           'specified domain')),
   537                "end of the pattern.")),
   848     # Domain commands
   538            formatter_class=RawDescriptionHelpFormatter)
   849     'domainadd': cmd('domainadd', 'da', domain_add, 'fqdn [transport]',
   539     ll.add_argument('-p', help=_("the pattern to search for"),
   850                      _('create a new domain')),
   540                     metavar='PATTERN', dest='pattern')
   851     'domaindelete': cmd('domaindelete', 'dd', domain_delete, 'fqdn [force]',
   541     ll.set_defaults(func=list_addresses, scmd='listaddresses')
   852                       _('delete the given domain and all its alias domains')),
   542 
   853     'domaininfo': cmd('domaininfo', 'di', domain_info, 'fqdn [details]',
   543     la = a('listaliases', aliases=('la',),
   854                       _('display information about the given domain')),
   544            help=_('list all aliases or search for aliases by pattern'),
   855     'domainquota': cmd('domainquota', 'dq', domain_quota,
   545            epilog=fill(_("This command lists all defined aliases. Aliases "
   856                        'fqdn storage [messages] [force]',
   546                "belonging to alias-domains are prefixed with a '-', addresses "
   857                        _('update the quota limit of the specified domain')),
   547                "of regular domains with a '+'. The output can be limited "
   858     'domainservices': cmd('domainservices', 'ds', domain_services,
   548                "with an optional pattern.\n\nTo perform a wild card search, "
   859                           'fqdn [service ...] [force]',
   549                "the % character can be used at the start and/or the end of "
   860                           _('enables the specified services and disables all '
   550                "the pattern.")),
   861                             'not specified services of the given domain')),
   551            formatter_class=RawDescriptionHelpFormatter)
   862     'domaintransport': cmd('domaintransport', 'dt', domain_transport,
   552     la.add_argument('-p', help=_("the pattern to search for"),
   863                            'fqdn transport [force]',
   553                     metavar='PATTERN', dest='pattern')
   864                            _('update the transport of the specified domain')),
   554     la.set_defaults(func=list_aliases, scmd='listaliases')
   865     'domainnote': cmd('domainnote', 'do', domain_note, 'fqdn [note]',
   555 
   866                      _('set, update or delete the note of the given domain')),
   556     ld = a('listdomains', aliases=('ld',),
   867     # List commands
   557            help=_('list all domains or search for domains by pattern'),
   868     'listdomains': cmd('listdomains', 'ld', list_domains, '[pattern]',
   558            epilog=fill(_("This subcommand lists all available domains. All "
   869                       _('list all domains or search for domains by pattern')),
   559                "domain names will be prefixed either with `[+]', if the "
   870     'listaddresses': cmd('listaddresses', 'll', list_addresses, '[pattern]',
   560                "domain is a primary domain, or with `[-]', if it is an alias "
   871                          _('list all addresses or search for addresses by '
   561                "domain name. The output can be limited with an optional "
   872                            'pattern')),
   562                "pattern.\n\nTo perform a wild card search, the % character "
   873     'listusers': cmd('listusers', 'lu', list_users, '[pattern]',
   563                "can be used at the start and/or the end of the pattern.")),
   874                      _('list all user accounts or search for accounts by '
   564            formatter_class=RawDescriptionHelpFormatter)
   875                        'pattern')),
   565     ld.add_argument('-p', help=_("the pattern to search for"),
   876     'listaliases': cmd('listaliases', 'la', list_aliases, '[pattern]',
   566                     metavar='PATTERN', dest='pattern')
   877                       _('list all aliases or search for aliases by pattern')),
   567     ld.set_defaults(func=list_domains, scmd='listdomains')
   878     'listrelocated': cmd('listrelocated', 'lr', list_relocated, '[pattern]',
   568 
   879                          _('list all relocated users or search for relocated '
   569     lr = a('listrelocated', aliases=('lr',),
   880                            'users by pattern')),
   570            help=_('list all relocated users or search for relocated users by '
   881     # Relocated commands
   571                   'pattern'),
   882     'relocatedadd': cmd('relocatedadd', 'ra', relocated_add,
   572            epilog=fill(_("This command lists all defined relocated addresses. "
   883                         'address newaddress',
   573                "Relocated entries belonging to alias-domains are prefixed "
   884                         _('create a new record for a relocated user')),
   574                "with a '-', addresses of regular domains with a '+'. The "
   885     'relocateddelete': cmd('relocateddelete', 'rd', relocated_delete,
   575                "output can be limited with an optional pattern.\n\nTo "
   886                            'address',
   576                "perform a wild card search, the % character can be used at "
   887                            _('delete the record of the relocated user')),
   577                "the start and/or the end of the pattern.")),
   888     'relocatedinfo': cmd('relocatedinfo', 'ri', relocated_info, 'address',
   578            formatter_class=RawDescriptionHelpFormatter)
   889                          _('print information about a relocated user')),
   579     lr.add_argument('-p', help=_("the pattern to search for"),
   890     # cli commands
   580                     metavar='PATTERN', dest='pattern')
   891     'configget': cmd('configget', 'cg', config_get, 'option',
   581     lr.set_defaults(func=list_relocated, scmd='listrelocated')
   892                      _('show the actual value of the configuration option')),
   582 
   893     'configset': cmd('configset', 'cs', config_set, 'option value',
   583     lu = a('listusers', aliases=('lu',),
   894                       _('set a new value for the configuration option')),
   584            help=_('list all user accounts or search for accounts by pattern'),
   895     'configure': cmd('configure', 'cf', configure, '[section]',
   585            epilog=fill(_("This command lists all user accounts. User accounts "
   896                      _('start interactive configuration mode')),
   586                "belonging to alias-domains are prefixed with a '-', "
   897     'listpwschemes': cmd('listpwschemes', 'lp', list_pwschemes, '',
   587                "addresses of regular domains with a '+'. The output can be "
   898                          _('lists all usable password schemes and password '
   588                "limited with an optional pattern.\n\nTo perform a wild card "
   899                            'encoding suffixes')),
   589                "search, the % character can be used at the start and/or the "
   900     'help': cmd('help', 'h', help_, '[subcommand]',
   590                "end of the pattern.")),
   901                 _('show a help overview or help for the given subcommand')),
   591            formatter_class=RawDescriptionHelpFormatter)
   902     'version': cmd('version', 'v', version, '',
   592     lu.add_argument('-p', help=_("the pattern to search for"),
   903                    _('show version and copyright information')),
   593                     metavar='PATTERN', dest='pattern')
   904     })
   594     lu.set_defaults(func=list_users, scmd='listusers')
       
   595 
       
   596     lp = a('listpwschemes', aliases=('lp',),
       
   597            help=_('lists all usable password schemes and password encoding '
       
   598                   'suffixes'),
       
   599            epilog=fill(_("This subcommand lists all password schemes which "
       
   600                "could be used in the vmm.cfg as value of the "
       
   601                "misc.password_scheme option. The output varies, depending "
       
   602                "on the used Dovecot version and the system's libc.\nWhen "
       
   603                "your Dovecot installation isn't too old, you will see "
       
   604                "additionally a few usable encoding suffixes. One of them can "
       
   605                "be appended to the password scheme.")),
       
   606            formatter_class=RawDescriptionHelpFormatter)
       
   607     lp.set_defaults(func=list_pwschemes, scmd='listpwschemes')
       
   608 
       
   609     ###
       
   610     # domain subcommands
       
   611     ###
       
   612     da = a('domainadd', aliases=('da',), help=_('create a new domain'),
       
   613            epilog=fill(_("Adds the new domain into the database and creates "
       
   614                "the domain directory.\n\nIf the optional argument transport "
       
   615                "is given, it will override the default transport "
       
   616                "(domain.transport) from vmm.cfg. The specified transport "
       
   617                "will be the default transport for all new accounts in this "
       
   618                "domain.")),
       
   619            formatter_class=RawDescriptionHelpFormatter)
       
   620     da.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   621     da.add_argument('-t', metavar='TRANSPORT', dest='transport',
       
   622                     help=_('a Postfix transport (transport: or '
       
   623                            'transport:nexthop)'))
       
   624     da.set_defaults(func=domain_add, scmd='domainadd')
       
   625 
       
   626     details = ('accounts', 'aliasdomains', 'aliases', 'catchall', 'relocated',
       
   627                'full')
       
   628     di = a('domaininfo', aliases=('di',),
       
   629            help=_('display information about the given domain'),
       
   630            epilog=fill(_("This subcommand shows some information about the "
       
   631                "given domain.\n\nFor a more detailed information about the "
       
   632                "domain the optional argument details can be specified. A "
       
   633                "possible details value can be one of the following six "
       
   634                "keywords:\n") + mklst(details)),
       
   635            formatter_class=RawDescriptionHelpFormatter)
       
   636     di.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   637     di.add_argument('-d', choices=details, dest='details', metavar='DETAILS',
       
   638                     help=_('additionally details to display'))
       
   639     di.set_defaults(func=domain_info, scmd='domaininfo')
       
   640 
       
   641     do = a('domainnote', aliases=('do',),
       
   642            help=_('set, update or delete the note of the given domain'),
       
   643            epilog=_('With this subcommand, it is possible to attach a note to '
       
   644                     'the specified domain. Without an argument, an existing '
       
   645                     'note is removed.'))
       
   646     do.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   647     do.add_argument('-n', metavar='NOTE', dest='note',
       
   648                     help=_('the note that should be set'))
       
   649     do.set_defaults(func=domain_note, scmd='domainnote')
       
   650 
       
   651     dq = a('domainquota', aliases=('dq',),
       
   652            help=_('update the quota limit of the specified domain'),
       
   653            epilog=fill(_("This subcommand is used to configure a new quota "
       
   654                "limit for the accounts of the domain - not for the domain "
       
   655                "itself.\n\nThe default quota limit for accounts is defined "
       
   656                "in the vmm.cfg (domain.quota_bytes and "
       
   657                "domain.quota_messages).\n\nThe new quota limit will affect "
       
   658                "only those accounts for which the limit has not been "
       
   659                "overridden. If you want to restore the default to all "
       
   660                "accounts, you may pass the optional argument --force. When "
       
   661                "the argument messages was omitted the default number of "
       
   662                "messages 0 (zero) will be applied.")),
       
   663            formatter_class=RawDescriptionHelpFormatter)
       
   664     dq.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   665     dq.add_argument('storage', type=quota_storage,
       
   666                     help=_('quota limit in {kilo,mega,giga}bytes e.g. 2G '
       
   667                            'or 2048M',))
       
   668     dq.add_argument('-m', default=0, type=int, metavar='MESSAGES',
       
   669                     dest='messages',
       
   670                     help=_('quota limit in number of messages (default: 0)'))
       
   671     dq.add_argument('--force', action='store_true',
       
   672                     help=_('enforce the limit for all accounts'))
       
   673     dq.set_defaults(func=domain_quota, scmd='domainquota')
       
   674 
       
   675     ds = a('domainservices', aliases=('ds',),
       
   676            help=_('enables the specified services and disables all not '
       
   677                   'specified services of the given domain'),
       
   678            epilog=fill(_("To define which services could be used by the users "
       
   679                "of the domain — with the given fqdn — use this "
       
   680                "subcommand.\n\nEach specified service will be enabled/"
       
   681                "usable. All other services will be deactivated/unusable. "
       
   682                "Possible service names are: imap, pop3, sieve and smtp.\nThe "
       
   683                "new service set will affect only those accounts for which "
       
   684                "the set has not been overridden. If you want to restore the "
       
   685                "default to all accounts, you may pass --force.")),
       
   686            formatter_class=RawDescriptionHelpFormatter)
       
   687     ds.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   688     ds.add_argument('-s', choices=SERVICES,
       
   689                     help=_('services which should be usable'),
       
   690                     metavar='SERVICE', nargs='+', dest='services')
       
   691     ds.add_argument('--force', action='store_true',
       
   692                     help=_('enforce the service set for all accounts'))
       
   693     ds.set_defaults(func=domain_services, scmd='domainservices')
       
   694 
       
   695     dt = a('domaintransport', aliases=('dt',),
       
   696            help=_('update the transport of the specified domain'),
       
   697            epilog=fill(_("A new transport for the indicated domain can be set "
       
   698                "with this subcommand.\n\nThe new transport will affect only "
       
   699                "those accounts for which the transport has not been "
       
   700                "overridden. If you want to restore the default to all "
       
   701                "accounts, you may pass --force.")),
       
   702            formatter_class=RawDescriptionHelpFormatter)
       
   703     dt.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   704     dt.add_argument('transport', help=_('a Postfix transport (transport: or '
       
   705                                         'transport:nexthop)'))
       
   706     dt.add_argument('--force', action='store_true',
       
   707                     help=_('enforce the transport for all accounts'))
       
   708     dt.set_defaults(func=domain_transport, scmd='domaintransport')
       
   709 
       
   710     dd = a('domaindelete', aliases=('dd',),
       
   711            help=_('delete the given domain and all its alias domains'),
       
   712            epilog=fill(_("This subcommand deletes the domain specified by "
       
   713                "fqdn.\n\nIf there are accounts, aliases and/or relocated "
       
   714                "users assigned to the given domain, vmm will abort the "
       
   715                "requested operation and show an error message. If you know, "
       
   716                "what you are doing, you can specify the optional argument "
       
   717                "--force.\n\nIf you really always know what you are doing, "
       
   718                "edit your vmm.cfg and set the option domain.force_deletion "
       
   719                "to true.")),
       
   720            formatter_class=RawDescriptionHelpFormatter)
       
   721     dd.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   722     dd.add_argument('--force', action='store_true',
       
   723                     help=_('also delete all accounts, aliases and/or '
       
   724                            'relocated users'))
       
   725     dd.set_defaults(func=domain_delete, scmd='domaindelete')
       
   726 
       
   727     ###
       
   728     # alias domain subcommands
       
   729     ###
       
   730     ada = a('aliasdomainadd', aliases=('ada',),
       
   731             help=_('create a new alias for an existing domain'),
       
   732             epilog=_('This subcommand adds the new alias domain (fqdn) to '
       
   733                      'the destination domain that should be aliased.'))
       
   734     ada.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   735     ada.add_argument('destination',
       
   736                      help=_('the fqdn of the destination domain'))
       
   737     ada.set_defaults(func=aliasdomain_add, scmd='aliasdomainadd')
       
   738 
       
   739     adi = a('aliasdomaininfo', aliases=('adi',),
       
   740             help=_('show the destination of the given alias domain'),
       
   741             epilog=_('This subcommand shows to which domain the alias domain '
       
   742                      'fqdn is assigned to.'))
       
   743     adi.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   744     adi.set_defaults(func=aliasdomain_info, scmd='aliasdomaininfo')
       
   745 
       
   746     ads = a('aliasdomainswitch', aliases=('ads',),
       
   747             help=_('assign the given alias domain to an other domain'),
       
   748             epilog=_('If the destination of the existing alias domain fqdn '
       
   749                      'should be switched to another destination use this '
       
   750                      'subcommand.'))
       
   751     ads.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   752     ads.add_argument('destination',
       
   753                      help=_('the fqdn of the destination domain'))
       
   754     ads.set_defaults(func=aliasdomain_switch, scmd='aliasdomainswitch')
       
   755 
       
   756     add = a('aliasdomaindelete', aliases=('add',),
       
   757             help=_('delete the specified alias domain'),
       
   758             epilog=_('Use this subcommand if the alias domain fqdn should be '
       
   759                      'removed.'))
       
   760     add.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   761     add.set_defaults(func=aliasdomain_delete, scmd='aliasdomaindelete')
       
   762 
       
   763     ###
       
   764     # account subcommands
       
   765     ###
       
   766     ua = a('useradd', aliases=('ua',),
       
   767            help=_('create a new e-mail user with the given address'),
       
   768            epilog=fill(_('Use this subcommand to create a new e-mail account '
       
   769                'for the given address.\n\nIf the password is not provided, '
       
   770                'vmm will prompt for it interactively. When no password is '
       
   771                'provided and account.random_password is set to true, vmm '
       
   772                'will generate a random password and print it to stdout '
       
   773                'after the account has been created.')),
       
   774            formatter_class=RawDescriptionHelpFormatter)
       
   775     ua.add_argument('address',
       
   776                     help=_("an account's e-mail address (local-part@fqdn)"))
       
   777     ua.add_argument('-p', metavar='PASSWORD', dest='password',
       
   778                     help=_("the new user's password"))
       
   779     ua.set_defaults(func=user_add, scmd='useradd')
       
   780 
       
   781     details = ('aliases', 'du', 'full')
       
   782     ui = a('userinfo', aliases=('ui',),
       
   783            help=_('display information about the given address'),
       
   784            epilog=fill(_('This subcommand displays some information about '
       
   785                'the account specified by the given address.\n\nIf the '
       
   786                'optional argument details is given some more information '
       
   787                'will be displayed.\nPossible values for details are:\n') +
       
   788                mklst(details)),
       
   789            formatter_class=RawDescriptionHelpFormatter)
       
   790     ui.add_argument('address',
       
   791                     help=_("an account's e-mail address (local-part@fqdn)"))
       
   792     ui.add_argument('-d', choices=details, metavar='DETAILS', dest='details',
       
   793                     help=_('additionally details to display'))
       
   794     ui.set_defaults(func=user_info, scmd='userinfo')
       
   795 
       
   796     un = a('username', aliases=('un',),
       
   797            help=_('set, update or delete the real name for an address'),
       
   798            epilog=fill(_("The user's real name can be set/updated with this "
       
   799                "subcommand.\n\nIf no name is given, the value stored for the "
       
   800                "account is erased.")),
       
   801            formatter_class=RawDescriptionHelpFormatter)
       
   802     un.add_argument('address',
       
   803                     help=_("an account's e-mail address (local-part@fqdn)"))
       
   804     un.add_argument('-n', help=_("a user's real name"), metavar='NAME',
       
   805                     dest='name')
       
   806     un.set_defaults(func=user_name, scmd='username')
       
   807 
       
   808     uo = a('usernote', aliases=('uo',),
       
   809            help=_('set, update or delete the note of the given address'),
       
   810            epilog=_('With this subcommand, it is possible to attach a note to '
       
   811                'the specified account. Without the note argument, an '
       
   812                'existing note is removed.'))
       
   813     uo.add_argument('address',
       
   814                     help=_("an account's e-mail address (local-part@fqdn)"))
       
   815     uo.add_argument('-n', metavar='NOTE', dest='note',
       
   816                     help=_('the note that should be set'))
       
   817     uo.set_defaults(func=user_note, scmd='usernote')
       
   818 
       
   819     up = a('userpassword', aliases=('up',),
       
   820            help=_('update the password for the given address'),
       
   821            epilog=fill(_("The password of an account can be updated with this "
       
   822                "subcommand.\n\nIf no password was provided, vmm will prompt "
       
   823                "for it interactively.")),
       
   824            formatter_class=RawDescriptionHelpFormatter)
       
   825     up.add_argument('address',
       
   826                     help=_("an account's e-mail address (local-part@fqdn)"))
       
   827     up.add_argument('-p', metavar='PASSWORD', dest='password',
       
   828                     help=_("the user's new password"))
       
   829     up.set_defaults(func=user_password, scmd='userpassword')
       
   830 
       
   831     uq = a('userquota', aliases=('uq',),
       
   832            help=_('update the quota limit for the given address'),
       
   833            epilog=fill(_("This subcommand is used to set a new quota limit "
       
   834                "for the given account.\n\nWhen the argument messages was "
       
   835                "omitted the default number of messages 0 (zero) will be "
       
   836                "applied.\n\nInstead of a storage limit pass the keyword "
       
   837                "'domain' to remove the account-specific override, causing "
       
   838                "the domain's value to be in effect.")),
       
   839            formatter_class=RawDescriptionHelpFormatter)
       
   840     uq.add_argument('address',
       
   841                     help=_("an account's e-mail address (local-part@fqdn)"))
       
   842     uq.add_argument('storage', type=quota_storage,
       
   843                     help=_('quota limit in {kilo,mega,giga}bytes e.g. 2G '
       
   844                            'or 2048M'))
       
   845     uq.add_argument('-m', default=0, type=int, metavar='MESSAGES',
       
   846                     dest='messages',
       
   847                     help=_('quota limit in number of messages (default: 0)'))
       
   848     uq.set_defaults(func=user_quota, scmd='userquota')
       
   849 
       
   850     us = a('userservices', aliases=('us',),
       
   851            help=_('enable the specified services and disables all not '
       
   852                   'specified services'),
       
   853            epilog=fill(_("To grant a user access to the specified service(s), "
       
   854                "use this command.\n\nAll omitted services will be "
       
   855                "deactivated/unusable for the user with the given "
       
   856                "address.\n\nInstead of any service pass the keyword "
       
   857                "'domain' to remove the account-specific override, causing "
       
   858                "the domain's value to be in effect.")),
       
   859            formatter_class=RawDescriptionHelpFormatter)
       
   860     us.add_argument('address',
       
   861                     help=_("an account's e-mail address (local-part@fqdn)"))
       
   862     us.add_argument('-s', choices=SERVICES + ('domain',),
       
   863                     help=_('services which should be usable'),
       
   864                     metavar='SERVICE', nargs='+', dest='services')
       
   865     us.set_defaults(func=user_services, scmd='userservices')
       
   866 
       
   867     ut = a('usertransport', aliases=('ut',),
       
   868            help=_('update the transport of the given address'),
       
   869            epilog=fill(_("A different transport for an account can be "
       
   870                "specified with this subcommand.\n\nInstead of a transport "
       
   871                "pass the keyword 'domain' to remove the account-specific "
       
   872                "override, causing the domain's value to be in effect.")),
       
   873            formatter_class=RawDescriptionHelpFormatter)
       
   874     ut.add_argument('address',
       
   875                     help=_("an account's e-mail address (local-part@fqdn)"))
       
   876     ut.add_argument('transport', help=_('a Postfix transport (transport: or '
       
   877                                         'transport:nexthop)'))
       
   878     ut.set_defaults(func=user_transport, scmd='usertransport')
       
   879 
       
   880     ud = a('userdelete', aliases=('ud',),
       
   881            help=_('delete the specified user'),
       
   882            epilog=fill(_('Use this subcommand to delete the account with the '
       
   883                'given address.\n\nIf there are one or more aliases with an '
       
   884                'identical destination address, vmm will abort the requested '
       
   885                'operation and show an error message. To prevent this, '
       
   886                'give the optional argument --force.')),
       
   887            formatter_class=RawDescriptionHelpFormatter)
       
   888     ud.add_argument('address',
       
   889                     help=_("an account's e-mail address (local-part@fqdn)"))
       
   890     ud.add_argument('--force', action='store_true',
       
   891                     help=_('also delete assigned alias addresses'))
       
   892     ud.set_defaults(func=user_delete, scmd='userdelete')
       
   893 
       
   894     ###
       
   895     # alias subcommands
       
   896     ###
       
   897     aa = a('aliasadd', aliases=('aa',),
       
   898            help=_('create a new alias e-mail address with one or more '
       
   899                   'destinations'),
       
   900            epilog=fill(_("This subcommand is used to create a new alias "
       
   901                "address with one or more destination addresses.\n\nWithin "
       
   902                "the destination address, the placeholders %n, %d, and %= "
       
   903                "will be replaced by the local part, the domain, or the "
       
   904                "email address with '@' replaced by '=' respectively. In "
       
   905                "combination with alias domains, this enables "
       
   906                "domain-specific destinations.")),
       
   907            formatter_class=RawDescriptionHelpFormatter)
       
   908     aa.add_argument('address',
       
   909                     help=_("an alias' e-mail address (local-part@fqdn)"))
       
   910     aa.add_argument('destination', nargs='+',
       
   911                     help=_("a destination's e-mail address (local-part@fqdn)"))
       
   912     aa.set_defaults(func=alias_add, scmd='aliasadd')
       
   913 
       
   914     ai = a('aliasinfo', aliases=('ai',),
       
   915            help=_('show the destination(s) of the specified alias'),
       
   916            epilog=_('Information about the alias with the given address can '
       
   917                     'be displayed with this subcommand.'))
       
   918     ai.add_argument('address',
       
   919                     help=_("an alias' e-mail address (local-part@fqdn)"))
       
   920     ai.set_defaults(func=alias_info, scmd='aliasinfo')
       
   921 
       
   922     ad = a('aliasdelete', aliases=('ad',),
       
   923            help=_('delete the specified alias e-mail address or one of its '
       
   924                   'destinations'),
       
   925            epilog=fill(_("This subcommand is used to delete one or multiple "
       
   926                "destinations from the alias with the given address.\n\nWhen "
       
   927                "no destination address was specified the alias with all its "
       
   928                "destinations will be deleted.")),
       
   929            formatter_class=RawDescriptionHelpFormatter)
       
   930     ad.add_argument('address',
       
   931                     help=_("an alias' e-mail address (local-part@fqdn)"))
       
   932     ad.add_argument('destination', nargs='*',
       
   933                     help=_("a destination's e-mail address (local-part@fqdn)"))
       
   934     ad.set_defaults(func=alias_delete, scmd='aliasdelete')
       
   935 
       
   936     ###
       
   937     # catch-all subcommands
       
   938     ###
       
   939     caa = a('catchalladd', aliases=('caa',),
       
   940             help=_('add one or more catch-all destinations for a domain'),
       
   941             epilog=fill(_('This subcommand allows to specify destination '
       
   942                 'addresses for a domain, which shall receive mail addressed '
       
   943                 'to unknown local parts within that domain. Those catch-all '
       
   944                 'aliases hence "catch all" mail to any address in the domain '
       
   945                 '(unless a more specific alias, mailbox or relocated entry '
       
   946                 'exists).\n\nWARNING: Catch-all addresses can cause mail '
       
   947                 'server flooding because spammers like to deliver mail to '
       
   948                 'all possible combinations of names, e.g. to all addresses '
       
   949                 'between abba@example.org and zztop@example.org.')),
       
   950            formatter_class=RawDescriptionHelpFormatter)
       
   951     caa.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   952     caa.add_argument('destination', nargs='+',
       
   953                     help=_("a destination's e-mail address (local-part@fqdn)"))
       
   954     caa.set_defaults(func=catchall_add, scmd='catchalladd')
       
   955 
       
   956     cai = a('catchallinfo', aliases=('cai',),
       
   957             help=_('show the catch-all destination(s) of the specified '
       
   958                    'domain'),
       
   959             epilog=_('This subcommand displays information about catch-all '
       
   960                      'aliases defined for a domain.'))
       
   961     cai.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   962     cai.set_defaults(func=catchall_info, scmd='catchallinfo')
       
   963 
       
   964     cad = a('catchalldelete', aliases=('cad',),
       
   965             help=_("delete the specified catch-all destination or all of a "
       
   966                    "domain's destinations"),
       
   967             epilog=_('With this subcommand, catch-all aliases defined for a '
       
   968                      'domain can be removed, either all of them, or those '
       
   969                      'destinations which were specified explicitly.'))
       
   970     cad.add_argument('fqdn', help=_('a fully qualified domain name'))
       
   971     cad.add_argument('destination', nargs='*',
       
   972                     help=_("a destination's e-mail address (local-part@fqdn)"))
       
   973     cad.set_defaults(func=catchall_delete, scmd='catchalldelete')
       
   974 
       
   975     ###
       
   976     # relocated subcommands
       
   977     ###
       
   978     ra = a('relocatedadd', aliases=('ra',),
       
   979            help=_('create a new record for a relocated user'),
       
   980            epilog=_("A new relocated user can be created with this "
       
   981                     "subcommand."))
       
   982     ra.add_argument('address', help=_("a relocated user's e-mail address "
       
   983                                       "(local-part@fqdn)"))
       
   984     ra.add_argument('newaddress',
       
   985                    help=_('e-mail address where the user can be reached now'))
       
   986     ra.set_defaults(func=relocated_add, scmd='relocatedadd')
       
   987 
       
   988     ri = a('relocatedinfo', aliases=('ri',),
       
   989            help=_('print information about a relocated user'),
       
   990            epilog=_('This subcommand shows the new address of the relocated '
       
   991                     'user with the given address.'))
       
   992     ri.add_argument('address', help=_("a relocated user's e-mail address "
       
   993                                       "(local-part@fqdn)"))
       
   994     ri.set_defaults(func=relocated_info, scmd='relocatedinfo')
       
   995 
       
   996     rd = a('relocateddelete', aliases=('rd',),
       
   997            help=_('delete the record of the relocated user'),
       
   998            epilog=_('Use this subcommand in order to delete the relocated '
       
   999                     'user with the given address.'))
       
  1000     rd.add_argument('address', help=_("a relocated user's e-mail address "
       
  1001                                       "(local-part@fqdn)"))
       
  1002     rd.set_defaults(func=relocated_delete, scmd='relocateddelete')
       
  1003 
       
  1004     txt_wrpr.replace_whitespace = old_rw
       
  1005     return parser
   905 
  1006 
   906 
  1007 
   907 def _get_order(ctx):
  1008 def _get_order(ctx):
   908     """returns a tuple with (key, 1||0) tuples. Used by functions, which
  1009     """returns a tuple with (key, 1||0) tuples. Used by functions, which
   909     get a dict from the handler."""
  1010     get a dict from the handler."""
   910     order = ()
  1011     order = ()
   911     if ctx.scmd == 'domaininfo':
  1012     if ctx.args.scmd == 'domaininfo':
   912         order = (('domain name', 0), ('gid', 1), ('domain directory', 0),
  1013         order = (('domain name', 0), ('gid', 1), ('domain directory', 0),
   913                  ('quota limit/user', 0), ('active services', 0),
  1014                  ('quota limit/user', 0), ('active services', 0),
   914                  ('transport', 0), ('alias domains', 0), ('accounts', 0),
  1015                  ('transport', 0), ('alias domains', 0), ('accounts', 0),
   915                  ('aliases', 0), ('relocated', 0), ('catch-all dests', 0))
  1016                  ('aliases', 0), ('relocated', 0), ('catch-all dests', 0))
   916     elif ctx.scmd == 'userinfo':
  1017     elif ctx.args.scmd == 'userinfo':
   917         if ctx.argc == 4 and ctx.args[3] != 'aliases' or \
  1018         if ctx.args.details in ('du', 'full') or \
   918            ctx.cget('account.disk_usage'):
  1019            ctx.cget('account.disk_usage'):
   919             order = (('address', 0), ('name', 0), ('uid', 1), ('gid', 1),
  1020             order = (('address', 0), ('name', 0), ('uid', 1), ('gid', 1),
   920                      ('home', 0), ('mail_location', 0),
  1021                      ('home', 0), ('mail_location', 0),
   921                      ('quota storage', 0), ('quota messages', 0),
  1022                      ('quota storage', 0), ('quota messages', 0),
   922                      ('disk usage', 0), ('transport', 0), ('smtp', 1),
  1023                      ('disk usage', 0), ('transport', 0), ('smtp', 1),
   925             order = (('address', 0), ('name', 0), ('uid', 1), ('gid', 1),
  1026             order = (('address', 0), ('name', 0), ('uid', 1), ('gid', 1),
   926                      ('home', 0), ('mail_location', 0),
  1027                      ('home', 0), ('mail_location', 0),
   927                      ('quota storage', 0), ('quota messages', 0),
  1028                      ('quota storage', 0), ('quota messages', 0),
   928                      ('transport', 0), ('smtp', 1), ('pop3', 1),
  1029                      ('transport', 0), ('smtp', 1), ('pop3', 1),
   929                      ('imap', 1), ('sieve', 1))
  1030                      ('imap', 1), ('sieve', 1))
   930     elif ctx.scmd == 'getuser':
  1031     elif ctx.args.scmd == 'getuser':
   931         order = (('uid', 1), ('gid', 1), ('address', 0))
  1032         order = (('uid', 1), ('gid', 1), ('address', 0))
   932     return order
  1033     return order
   933 
  1034 
   934 
  1035 
   935 def _format_quota_usage(limit, used, human=False, domaindefault=False):
  1036 def _format_quota_usage(limit, used, human=False, domaindefault=False):