diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/cli/subcommands.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/cli/subcommands.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,1091 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.cli.subcommands + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + VirtualMailManager's cli subcommands. +""" + +import locale +import os + +from textwrap import TextWrapper +from time import strftime, strptime + +from VirtualMailManager import ENCODING +from VirtualMailManager.cli import get_winsize, prog, w_err, w_std +from VirtualMailManager.common import human_size, size_in_bytes, \ + version_str, format_domain_default +from VirtualMailManager.constants import __copyright__, __date__, \ + __version__, ACCOUNT_EXISTS, ALIAS_EXISTS, ALIASDOMAIN_ISDOMAIN, \ + DOMAIN_ALIAS_EXISTS, INVALID_ARGUMENT, EX_MISSING_ARGS, \ + RELOCATED_EXISTS, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED +from VirtualMailManager.errors import VMMError +from VirtualMailManager.password import list_schemes +from VirtualMailManager.serviceset import SERVICES + +__all__ = ( + 'Command', 'RunContext', 'cmd_map', 'usage', 'alias_add', 'alias_delete', + 'alias_info', 'aliasdomain_add', 'aliasdomain_delete', 'aliasdomain_info', + 'aliasdomain_switch', 'catchall_add', 'catchall_info', 'catchall_delete', + 'config_get', 'config_set', 'configure', + 'domain_add', 'domain_delete', 'domain_info', 'domain_quota', + 'domain_services', 'domain_transport', 'domain_note', 'get_user', 'help_', + 'list_domains', 'list_pwschemes', 'list_users', 'list_aliases', + 'list_relocated', 'list_addresses', 'relocated_add', 'relocated_delete', + 'relocated_info', 'user_add', 'user_delete', 'user_info', 'user_name', + 'user_password', 'user_quota', 'user_services', 'user_transport', + 'user_note', 'version', +) + +_ = lambda msg: msg +txt_wrpr = TextWrapper(width=get_winsize()[1] - 1) +cmd_map = {} + + +class Command(object): + """Container class for command information.""" + __slots__ = ('name', 'alias', 'func', 'args', 'descr') + + def __init__(self, name, alias, func, args, descr): + """Create a new Command instance. + + Arguments: + + `name` : str + the command name, e.g. ``addalias`` + `alias` : str + the command's short alias, e.g. ``aa`` + `func` : callable + the function to handle the command + `args` : str + argument placeholders, e.g. ``aliasaddress`` + `descr` : str + short description of the command + """ + self.name = name + self.alias = alias + self.func = func + self.args = args + self.descr = descr + + @property + def usage(self): + """the command's usage info.""" + return u'%s %s %s' % (prog, self.name, self.args) + + +class RunContext(object): + """Contains all information necessary to run a subcommand.""" + __slots__ = ('argc', 'args', 'cget', 'hdlr', 'scmd') + plan_a_b = _(u'Plan A failed ... trying Plan B: %(subcommand)s %(object)s') + + def __init__(self, argv, handler, command): + """Create a new RunContext""" + self.argc = len(argv) + self.args = [unicode(arg, ENCODING) for arg in argv] + self.cget = handler.cfg_dget + self.hdlr = handler + self.scmd = command + + +def alias_add(ctx): + """create a new alias e-mail address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias address and destination.'), + ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination address.'), ctx.scmd) + ctx.hdlr.alias_add(ctx.args[2].lower(), *ctx.args[3:]) + + +def alias_delete(ctx): + """delete the specified alias e-mail address or one of its destinations""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias address.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.alias_delete(ctx.args[2].lower()) + else: + ctx.hdlr.alias_delete(ctx.args[2].lower(), ctx.args[3]) + + +def alias_info(ctx): + """show the destination(s) of the specified alias""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias address.'), ctx.scmd) + address = ctx.args[2].lower() + try: + _print_aliase_info(address, ctx.hdlr.alias_info(address)) + except VMMError, err: + if err.code is ACCOUNT_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'userinfo', + 'object': address}) + ctx.scmd = ctx.args[1] = 'userinfo' + user_info(ctx) + elif err.code is RELOCATED_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'relocatedinfo', + 'object': address}) + ctx.scmd = ctx.args[1] = 'relocatedinfo' + relocated_info(ctx) + else: + raise + + +def aliasdomain_add(ctx): + """create a new alias for an existing domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias domain name and destination ' + u'domain name.'), ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination domain name.'), + ctx.scmd) + ctx.hdlr.aliasdomain_add(ctx.args[2].lower(), ctx.args[3].lower()) + + +def aliasdomain_delete(ctx): + """delete the specified alias domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias domain name.'), ctx.scmd) + ctx.hdlr.aliasdomain_delete(ctx.args[2].lower()) + + +def aliasdomain_info(ctx): + """show the destination of the given alias domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias domain name.'), ctx.scmd) + try: + _print_aliasdomain_info(ctx.hdlr.aliasdomain_info(ctx.args[2].lower())) + except VMMError, err: + if err.code is ALIASDOMAIN_ISDOMAIN: + w_err(0, ctx.plan_a_b % {'subcommand': u'domaininfo', + 'object': ctx.args[2].lower()}) + ctx.scmd = ctx.args[1] = 'domaininfo' + domain_info(ctx) + else: + raise + + +def aliasdomain_switch(ctx): + """assign the given alias domain to an other domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias domain name and destination ' + u'domain name.'), ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination domain name.'), + ctx.scmd) + ctx.hdlr.aliasdomain_switch(ctx.args[2].lower(), ctx.args[3].lower()) + + +def catchall_add(ctx): + """create a new catchall alias e-mail address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain and destination.'), + ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination address.'), ctx.scmd) + ctx.hdlr.catchall_add(ctx.args[2].lower(), *ctx.args[3:]) + + +def catchall_delete(ctx): + """delete the specified destination or all of the catchall destination""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.catchall_delete(ctx.args[2].lower()) + else: + ctx.hdlr.catchall_delete(ctx.args[2].lower(), ctx.args[3]) + + +def catchall_info(ctx): + """show the catchall destination(s) of the specified domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain.'), ctx.scmd) + address = ctx.args[2].lower() + _print_catchall_info(address, ctx.hdlr.catchall_info(address)) + + +def config_get(ctx): + """show the actual value of the configuration option""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u"Missing option name."), ctx.scmd) + + noop = lambda option: option + opt_formater = { + 'misc.dovecot_version': version_str, + 'domain.quota_bytes': human_size, + } + + option = ctx.args[2].lower() + w_std('%s = %s' % (option, opt_formater.get(option, + noop)(ctx.cget(option)))) + + +def config_set(ctx): + """set a new value for the configuration option""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing option and new value.'), ctx.scmd) + if ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing new configuration value.'), + ctx.scmd) + ctx.hdlr.cfg_set(ctx.args[2].lower(), ctx.args[3]) + + +def configure(ctx): + """start interactive configuration modus""" + if ctx.argc < 3: + ctx.hdlr.configure() + else: + ctx.hdlr.configure(ctx.args[2].lower()) + + +def domain_add(ctx): + """create a new domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.domain_add(ctx.args[2].lower()) + else: + ctx.hdlr.domain_add(ctx.args[2].lower(), ctx.args[3]) + if ctx.cget('domain.auto_postmaster'): + w_std(_(u'Creating account for postmaster@%s') % ctx.args[2].lower()) + ctx.scmd = 'useradd' + ctx.args = [prog, ctx.scmd, u'postmaster@' + ctx.args[2].lower()] + ctx.argc = 3 + user_add(ctx) + + +def domain_delete(ctx): + """delete the given domain and all its alias domains""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.domain_delete(ctx.args[2].lower()) + elif ctx.args[3].lower() == 'force': + ctx.hdlr.domain_delete(ctx.args[2].lower(), True) + else: + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % ctx.args[3], + ctx.scmd) + + +def domain_info(ctx): + """display information about the given domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), ctx.scmd) + if ctx.argc < 4: + details = None + else: + details = ctx.args[3].lower() + if details not in ('accounts', 'aliasdomains', 'aliases', 'full', + 'relocated', 'catchall'): + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % details, + ctx.scmd) + try: + info = ctx.hdlr.domain_info(ctx.args[2].lower(), details) + except VMMError, err: + if err.code is DOMAIN_ALIAS_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'aliasdomaininfo', + 'object': ctx.args[2].lower()}) + ctx.scmd = ctx.args[1] = 'aliasdomaininfo' + aliasdomain_info(ctx) + else: + raise + else: + q_limit = u'Storage: %(bytes)s; Messages: %(messages)s' + if not details: + info['bytes'] = human_size(info['bytes']) + info['messages'] = locale.format('%d', info['messages'], True) + info['quota limit/user'] = q_limit % info + _print_info(ctx, info, _(u'Domain')) + else: + info[0]['bytes'] = human_size(info[0]['bytes']) + info[0]['messages'] = locale.format('%d', info[0]['messages'], + True) + info[0]['quota limit/user'] = q_limit % info[0] + _print_info(ctx, info[0], _(u'Domain')) + if details == u'accounts': + _print_list(info[1], _(u'accounts')) + elif details == u'aliasdomains': + _print_list(info[1], _(u'alias domains')) + elif details == u'aliases': + _print_list(info[1], _(u'aliases')) + elif details == u'relocated': + _print_list(info[1], _(u'relocated users')) + elif details == u'catchall': + _print_list(info[1], _(u'catch-all destinations')) + else: + _print_list(info[1], _(u'alias domains')) + _print_list(info[2], _(u'accounts')) + _print_list(info[3], _(u'aliases')) + _print_list(info[4], _(u'relocated users')) + _print_list(info[5], _(u'catch-all destinations')) + + +def domain_quota(ctx): + """update the quota limit of the specified domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name and storage value.'), + ctx.scmd) + if ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing storage value.'), ctx.scmd) + messages = 0 + force = None + try: + bytes_ = size_in_bytes(ctx.args[3]) + except (ValueError, TypeError): + usage(INVALID_ARGUMENT, _(u"Invalid storage value: '%s'") % + ctx.args[3], ctx.scmd) + if ctx.argc < 5: + pass + elif ctx.argc < 6: + try: + messages = int(ctx.args[4]) + except ValueError: + if ctx.args[4].lower() != 'force': + usage(INVALID_ARGUMENT, + _(u"Neither a valid number of messages nor the keyword " + u"'force': '%s'") % ctx.args[4], ctx.scmd) + force = 'force' + else: + try: + messages = int(ctx.args[4]) + except ValueError: + usage(INVALID_ARGUMENT, + _(u"Not a valid number of messages: '%s'") % ctx.args[4], + ctx.scmd) + if ctx.args[5].lower() != 'force': + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % ctx.args[5], + ctx.scmd) + force = 'force' + ctx.hdlr.domain_quotalimit(ctx.args[2].lower(), bytes_, messages, force) + + +def domain_services(ctx): + """allow all named service and block the uncredited.""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), ctx.scmd) + services = [] + force = False + if ctx.argc is 3: + pass + elif ctx.argc is 4: + arg = ctx.args[3].lower() + if arg in SERVICES: + services.append(arg) + elif arg == 'force': + force = True + else: + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % arg, + ctx.scmd) + else: + services.extend([service.lower() for service in ctx.args[3:-1]]) + arg = ctx.args[-1].lower() + if arg == 'force': + force = True + else: + services.append(arg) + unknown = [service for service in services if service not in SERVICES] + if unknown: + usage(INVALID_ARGUMENT, _(u'Invalid service arguments: %s') % + ' '.join(unknown), ctx.scmd) + ctx.hdlr.domain_services(ctx.args[2].lower(), (None, 'force')[force], + *services) + + +def domain_transport(ctx): + """update the transport of the specified domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name and new transport.'), + ctx.scmd) + if ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing new transport.'), ctx.scmd) + if ctx.argc < 5: + ctx.hdlr.domain_transport(ctx.args[2].lower(), ctx.args[3]) + else: + force = ctx.args[4].lower() + if force != 'force': + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % force, + ctx.scmd) + ctx.hdlr.domain_transport(ctx.args[2].lower(), ctx.args[3], force) + + +def domain_note(ctx): + """update the note of the given domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), + ctx.scmd) + elif ctx.argc < 4: + note = None + else: + note = ' '.join(ctx.args[3:]) + ctx.hdlr.domain_note(ctx.args[2].lower(), note) + + +def get_user(ctx): + """get the address of the user with the given UID""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing UID.'), ctx.scmd) + _print_info(ctx, ctx.hdlr.user_by_uid(ctx.args[2]), _(u'Account')) + + +def help_(ctx): + """print help messages.""" + if ctx.argc > 2: + hlptpc = ctx.args[2].lower() + if hlptpc in cmd_map: + topic = hlptpc + else: + for scmd in cmd_map.itervalues(): + if scmd.alias == hlptpc: + topic = scmd.name + break + else: + usage(INVALID_ARGUMENT, _(u"Unknown help topic: '%s'") % + ctx.args[2], ctx.scmd) + # FIXME + w_err(1, "'help %s' not yet implemented." % topic, 'see also: vmm(1)') + + old_ii = txt_wrpr.initial_indent + old_si = txt_wrpr.subsequent_indent + txt_wrpr.initial_indent = ' ' + # len(max(_overview.iterkeys(), key=len)) #Py25 + txt_wrpr.subsequent_indent = 20 * ' ' + order = cmd_map.keys() + order.sort() + + w_std(_(u'List of available subcommands:') + '\n') + for key in order: + w_std('\n'.join(txt_wrpr.wrap('%-18s %s' % (key, cmd_map[key].descr)))) + + txt_wrpr.initial_indent = old_ii + txt_wrpr.subsequent_indent = old_si + txt_wrpr.initial_indent = '' + + +def list_domains(ctx): + """list all domains / search domains by pattern""" + matching = ctx.argc > 2 + if matching: + gids, domains = ctx.hdlr.domain_list(ctx.args[2].lower()) + else: + gids, domains = ctx.hdlr.domain_list() + _print_domain_list(gids, domains, matching) + + +def list_pwschemes(ctx_unused): + """Prints all usable password schemes and password encoding suffixes.""" + # TODO: Remove trailing colons from keys. + # For now it is to late, the translators has stared their work + keys = (_(u'Usable password schemes:'), _(u'Usable encoding suffixes:')) + old_ii, old_si = txt_wrpr.initial_indent, txt_wrpr.subsequent_indent + txt_wrpr.initial_indent = txt_wrpr.subsequent_indent = '\t' + txt_wrpr.width = txt_wrpr.width - 8 + + for key, value in zip(keys, list_schemes()): + if key.endswith(':'): # who knows … (see TODO above) + #key = key.rpartition(':')[0] + key = key[:-1] # This one is for Py24 + w_std(key, len(key) * '-') + w_std('\n'.join(txt_wrpr.wrap(' '.join(value))), '') + + txt_wrpr.initial_indent, txt_wrpr.subsequent_indent = old_ii, old_si + txt_wrpr.width = txt_wrpr.width + 8 + + +def list_addresses(ctx, limit=None): + """List all addresses / search addresses by pattern. The output can be + limited with TYPE_ACCOUNT, TYPE_ALIAS and TYPE_RELOCATED, which can be + bitwise ORed as a combination. Not specifying a limit is the same as + combining all three.""" + if limit is None: + limit = TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED + matching = ctx.argc > 2 + if matching: + gids, addresses = ctx.hdlr.address_list(limit, ctx.args[2].lower()) + else: + gids, addresses = ctx.hdlr.address_list(limit) + _print_address_list(limit, gids, addresses, matching) + + +def list_users(ctx): + """list all user accounts / search user accounts by pattern""" + return list_addresses(ctx, TYPE_ACCOUNT) + +def list_aliases(ctx): + """list all aliases / search aliases by pattern""" + return list_addresses(ctx, TYPE_ALIAS) + +def list_relocated(ctx): + """list all relocated records / search relocated records by pattern""" + return list_addresses(ctx, TYPE_RELOCATED) + + +def relocated_add(ctx): + """create a new record for a relocated user""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, + _(u'Missing relocated address and destination.'), ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination address.'), ctx.scmd) + ctx.hdlr.relocated_add(ctx.args[2].lower(), ctx.args[3]) + + +def relocated_delete(ctx): + """delete the record of the relocated user""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing relocated address.'), ctx.scmd) + ctx.hdlr.relocated_delete(ctx.args[2].lower()) + + +def relocated_info(ctx): + """print information about a relocated user""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing relocated address.'), ctx.scmd) + relocated = ctx.args[2].lower() + try: + _print_relocated_info(addr=relocated, + dest=ctx.hdlr.relocated_info(relocated)) + except VMMError, err: + if err.code is ACCOUNT_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'userinfo', + 'object': relocated}) + ctx.scmd = ctx.args[1] = 'userinfoi' + user_info(ctx) + elif err.code is ALIAS_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'aliasinfo', + 'object': relocated}) + ctx.scmd = ctx.args[1] = 'aliasinfo' + alias_info(ctx) + else: + raise + + +def user_add(ctx): + """create a new e-mail user with the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + elif ctx.argc < 4: + password = None + else: + password = ctx.args[3] + gen_pass = ctx.hdlr.user_add(ctx.args[2].lower(), password) + if ctx.argc < 4 and gen_pass: + w_std(_(u"Generated password: %s") % gen_pass) + + +def user_delete(ctx): + """delete the specified user""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.user_delete(ctx.args[2].lower()) + elif ctx.args[3].lower() == 'force': + ctx.hdlr.user_delete(ctx.args[2].lower(), True) + else: + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % ctx.args[3], + ctx.scmd) + + +def user_info(ctx): + """display information about the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + if ctx.argc < 4: + details = None + else: + details = ctx.args[3].lower() + if details not in ('aliases', 'du', 'full'): + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % details, + ctx.scmd) + try: + info = ctx.hdlr.user_info(ctx.args[2].lower(), details) + except VMMError, err: + if err.code is ALIAS_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'aliasinfo', + 'object': ctx.args[2].lower()}) + ctx.scmd = ctx.args[1] = 'aliasinfo' + alias_info(ctx) + elif err.code is RELOCATED_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'relocatedinfo', + 'object': ctx.args[2].lower()}) + ctx.scmd = ctx.args[1] = 'relocatedinfo' + relocated_info(ctx) + else: + raise + else: + if details in (None, 'du'): + info['quota storage'] = _format_quota_usage(info['ql_bytes'], + info['uq_bytes'], True, info['ql_domaindefault']) + info['quota messages'] = _format_quota_usage(info['ql_messages'], + info['uq_messages'], domaindefault=info['ql_domaindefault']) + _print_info(ctx, info, _(u'Account')) + else: + info[0]['quota storage'] = _format_quota_usage(info[0]['ql_bytes'], + info[0]['uq_bytes'], True, info[0]['ql_domaindefault']) + info[0]['quota messages'] = \ + _format_quota_usage(info[0]['ql_messages'], + info[0]['uq_messages'], + domaindefault=info[0]['ql_domaindefault']) + _print_info(ctx, info[0], _(u'Account')) + _print_list(info[1], _(u'alias addresses')) + + +def user_name(ctx): + """set or update the real name for an address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u"Missing e-mail address and user's name."), + ctx.scmd) + elif ctx.argc < 4: + name = None + else: + name = ctx.args[3] + ctx.hdlr.user_name(ctx.args[2].lower(), name) + + +def user_password(ctx): + """update the password for the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + elif ctx.argc < 4: + password = None + else: + password = ctx.args[3] + ctx.hdlr.user_password(ctx.args[2].lower(), password) + + +def user_note(ctx): + """update the note of the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), + ctx.scmd) + elif ctx.argc < 4: + note = None + else: + note = ' '.join(ctx.args[3:]) + ctx.hdlr.user_note(ctx.args[2].lower(), note) + + +def user_quota(ctx): + """update the quota limit for the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address and storage value.'), + ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing storage value.'), ctx.scmd) + if ctx.args[3] != 'domain': + try: + bytes_ = size_in_bytes(ctx.args[3]) + except (ValueError, TypeError): + usage(INVALID_ARGUMENT, _(u"Invalid storage value: '%s'") % + ctx.args[3], ctx.scmd) + else: + bytes_ = ctx.args[3] + if ctx.argc < 5: + messages = 0 + else: + try: + messages = int(ctx.args[4]) + except ValueError: + usage(INVALID_ARGUMENT, + _(u"Not a valid number of messages: '%s'") % ctx.args[4], + ctx.scmd) + ctx.hdlr.user_quotalimit(ctx.args[2].lower(), bytes_, messages) + + +def user_services(ctx): + """allow all named service and block the uncredited.""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + services = [] + if ctx.argc >= 4: + services.extend([service.lower() for service in ctx.args[3:]]) + unknown = [service for service in services if service not in SERVICES] + if unknown and ctx.args[3] != 'domain': + usage(INVALID_ARGUMENT, _(u'Invalid service arguments: %s') % + ' '.join(unknown), ctx.scmd) + ctx.hdlr.user_services(ctx.args[2].lower(), *services) + + +def user_transport(ctx): + """update the transport of the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address and transport.'), + ctx.scmd) + if ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing transport.'), ctx.scmd) + ctx.hdlr.user_transport(ctx.args[2].lower(), ctx.args[3]) + + +def usage(errno, errmsg, subcommand=None): + """print usage message for the given command or all commands. + When errno > 0, sys,exit(errno) will interrupt the program. + """ + if subcommand and subcommand in cmd_map: + w_err(errno, _(u"Error: %s") % errmsg, + _(u"usage: ") + cmd_map[subcommand].usage) + + # TP: Please adjust translated words like the original text. + # (It's a table header.) Extract from usage text: + # usage: vmm subcommand arguments + # short long + # subcommand arguments + # + # da domainadd fqdn [transport] + # dd domaindelete fqdn [force] + u_head = _(u"""usage: %s subcommand arguments + short long + subcommand arguments\n""") % prog + order = cmd_map.keys() + order.sort() + w_err(0, u_head) + for key in order: + scmd = cmd_map[key] + w_err(0, ' %-5s %-19s %s' % (scmd.alias, scmd.name, scmd.args)) + w_err(errno, '', _(u"Error: %s") % errmsg) + + +def version(ctx_unused): + """Write version and copyright information to stdout.""" + w_std('%s, %s %s (%s %s)\nPython %s %s %s\n\n%s\n%s %s' % (prog, + # TP: The words 'from', 'version' and 'on' are used in + # the version information, e.g.: + # vmm, version 0.5.2 (from 09/09/09) + # Python 2.5.4 on FreeBSD + _(u'version'), __version__, _(u'from'), + strftime(locale.nl_langinfo(locale.D_FMT), + strptime(__date__, '%Y-%m-%d')).decode(ENCODING, 'replace'), + os.sys.version.split()[0], _(u'on'), os.uname()[0], + __copyright__, prog, + _(u'is free software and comes with ABSOLUTELY NO WARRANTY.'))) + + +def update_cmd_map(): + """Update the cmd_map, after gettext's _ was installed.""" + cmd = Command + cmd_map.update({ + # Account commands + 'getuser': cmd('getuser', 'gu', get_user, 'uid', + _(u'get the address of the user with the given UID')), + 'useradd': cmd('useradd', 'ua', user_add, 'address [password]', + _(u'create a new e-mail user with the given address')), + 'userdelete': cmd('userdelete', 'ud', user_delete, 'address [force]', + _(u'delete the specified user')), + 'userinfo': cmd('userinfo', 'ui', user_info, 'address [details]', + _(u'display information about the given address')), + 'username': cmd('username', 'un', user_name, 'address name', + _(u'set or update the real name for an address')), + 'userpassword': cmd('userpassword', 'up', user_password, + 'address [password]', + _(u'update the password for the given address')), + 'userquota': cmd('userquota', 'uq', user_quota, + 'address storage [messages] | address domain', + _(u'update the quota limit for the given address')), + 'userservices': cmd('userservices', 'us', user_services, + 'address [service ...] | address domain', + _(u'enables the specified services and disables all ' + u'not specified services')), + 'usertransport': cmd('usertransport', 'ut', user_transport, + 'address transport | address domain', + _(u'update the transport of the given address')), + 'usernote': cmd('usernote', 'uo', user_note, + 'address note', + _(u'update the note of the given address')), + # Alias commands + 'aliasadd': cmd('aliasadd', 'aa', alias_add, 'address destination ...', + _(u'create a new alias e-mail address with one or more ' + u'destinations')), + 'aliasdelete': cmd('aliasdelete', 'ad', alias_delete, + 'address [destination]', + _(u'delete the specified alias e-mail address or one ' + u'of its destinations')), + 'aliasinfo': cmd('aliasinfo', 'ai', alias_info, 'address', + _(u'show the destination(s) of the specified alias')), + # AliasDomain commands + 'aliasdomainadd': cmd('aliasdomainadd', 'ada', aliasdomain_add, + 'fqdn destination', + _(u'create a new alias for an existing domain')), + 'aliasdomaindelete': cmd('aliasdomaindelete', 'add', aliasdomain_delete, + 'fqdn', _(u'delete the specified alias domain')), + 'aliasdomaininfo': cmd('aliasdomaininfo', 'adi', aliasdomain_info, 'fqdn', + _(u'show the destination of the given alias domain')), + 'aliasdomainswitch': cmd('aliasdomainswitch', 'ads', aliasdomain_switch, + 'fqdn destination', _(u'assign the given alias ' + 'domain to an other domain')), + # CatchallAlias commands + 'catchalladd': cmd('catchalladd', 'caa', catchall_add, + 'fqdn destination ...', + _(u'add one or more catch-all destinations for a ' + u'domain')), + 'catchalldelete': cmd('catchalldelete', 'cad', catchall_delete, + 'fqdn [destination]', + _(u'delete the specified catch-all destination or all ' + u'of a domain\'s destinations')), + 'catchallinfo': cmd('catchallinfo', 'cai', catchall_info, 'fqdn', + _(u'show the catch-all destination(s) of the specified domain')), + # Domain commands + 'domainadd': cmd('domainadd', 'da', domain_add, 'fqdn [transport]', + _(u'create a new domain')), + 'domaindelete': cmd('domaindelete', 'dd', domain_delete, 'fqdn [force]', + _(u'delete the given domain and all its alias domains')), + 'domaininfo': cmd('domaininfo', 'di', domain_info, 'fqdn [details]', + _(u'display information about the given domain')), + 'domainquota': cmd('domainquota', 'dq', domain_quota, + 'fqdn storage [messages] [force]', + _(u'update the quota limit of the specified domain')), + 'domainservices': cmd('domainservices', 'ds', domain_services, + 'fqdn [service ...] [force]', + _(u'enables the specified services and disables all ' + u'not specified services of the given domain')), + 'domaintransport': cmd('domaintransport', 'dt', domain_transport, + 'fqdn transport [force]', + _(u'update the transport of the specified domain')), + 'domainnote': cmd('domainnote', 'do', domain_note, + 'fqdn note', + _(u'update the note of the given domain')), + # List commands + 'listdomains': cmd('listdomains', 'ld', list_domains, '[pattern]', + _(u'list all domains or search for domains by pattern')), + 'listaddresses': cmd('listaddresses', 'll', list_addresses, '[pattern]', + _(u'list all addresses or search for addresses by pattern')), + 'listusers': cmd('listusers', 'lu', list_users, '[pattern]', + _(u'list all user accounts or search for accounts by pattern')), + 'listaliases': cmd('listaliases', 'la', list_aliases, '[pattern]', + _(u'list all aliases or search for aliases by pattern')), + 'listrelocated': cmd('listrelocated', 'lr', list_relocated, '[pattern]', + _(u'list all relocated entries or search for entries by pattern')), + # Relocated commands + 'relocatedadd': cmd('relocatedadd', 'ra', relocated_add, + 'address newaddress', + _(u'create a new record for a relocated user')), + 'relocateddelete': cmd('relocateddelete', 'rd', relocated_delete, + 'address', + _(u'delete the record of the relocated user')), + 'relocatedinfo': cmd('relocatedinfo', 'ri', relocated_info, 'address', + _(u'print information about a relocated user')), + # cli commands + 'configget': cmd('configget', 'cg', config_get, 'option', + _('show the actual value of the configuration option')), + 'configset': cmd('configset', 'cs', config_set, 'option value', + _('set a new value for the configuration option')), + 'configure': cmd('configure', 'cf', configure, '[section]', + _(u'start interactive configuration modus')), + 'listpwschemes': cmd('listpwschemes', 'lp', list_pwschemes, '', + _(u'lists all usable password schemes and password ' + u'encoding suffixes')), + 'help': cmd('help', 'h', help_, '[subcommand]', + _(u'show a help overview or help for the given subcommand')), + 'version': cmd('version', 'v', version, '', + _(u'show version and copyright information')), + }) + + +def _get_order(ctx): + """returns a tuple with (key, 1||0) tuples. Used by functions, which + get a dict from the handler.""" + order = () + if ctx.scmd == 'domaininfo': + order = ((u'domain name', 0), (u'gid', 1), (u'domain directory', 0), + (u'quota limit/user', 0), (u'active services', 0), + (u'transport', 0), (u'alias domains', 0), (u'accounts', 0), + (u'aliases', 0), (u'relocated', 0), (u'catch-all dests', 0)) + elif ctx.scmd == 'userinfo': + if ctx.argc == 4 and ctx.args[3] != u'aliases' or \ + ctx.cget('account.disk_usage'): + order = ((u'address', 0), (u'name', 0), (u'uid', 1), (u'gid', 1), + (u'home', 0), (u'mail_location', 0), + (u'quota storage', 0), (u'quota messages', 0), + (u'disk usage', 0), (u'transport', 0), (u'smtp', 1), + (u'pop3', 1), (u'imap', 1), (u'sieve', 1)) + else: + order = ((u'address', 0), (u'name', 0), (u'uid', 1), (u'gid', 1), + (u'home', 0), (u'mail_location', 0), + (u'quota storage', 0), (u'quota messages', 0), + (u'transport', 0), (u'smtp', 1), (u'pop3', 1), + (u'imap', 1), (u'sieve', 1)) + elif ctx.scmd == 'getuser': + order = ((u'uid', 1), (u'gid', 1), (u'address', 0)) + return order + + +def _format_quota_usage(limit, used, human=False, domaindefault=False): + """Put quota's limit / usage / percentage in a formatted string.""" + if human: + q_usage = { + 'used': human_size(used), + 'limit': human_size(limit), + } + else: + q_usage = { + 'used': locale.format('%d', used, True), + 'limit': locale.format('%d', limit, True), + } + if limit: + q_usage['percent'] = locale.format('%6.2f', 100. / limit * used, True) + else: + q_usage['percent'] = locale.format('%6.2f', 0, True) + # Py25: fmt = format_domain_default if domaindefault else lambda s: s + if domaindefault: + fmt = format_domain_default + else: + fmt = lambda s: s + return fmt(_(u'[%(percent)s%%] %(used)s/%(limit)s') % q_usage) + + +def _print_info(ctx, info, title): + """Print info dicts.""" + # TP: used in e.g. 'Domain information' or 'Account information' + msg = u'%s %s' % (title, _(u'information')) + w_std(msg, u'-' * len(msg)) + for key, upper in _get_order(ctx): + if upper: + w_std(u'\t%s: %s' % (key.upper().ljust(17, u'.'), info[key])) + else: + w_std(u'\t%s: %s' % (key.title().ljust(17, u'.'), info[key])) + print + note = info.get('note', None) + if note is not None: + _print_note(note) + +def _print_note(note): + msg = _(u'Note') + w_std(msg, u'-' * len(msg)) + old_ii = txt_wrpr.initial_indent + old_si = txt_wrpr.subsequent_indent + txt_wrpr.initial_indent = txt_wrpr.subsequent_indent = '\t' + txt_wrpr.width -= 8 + for para in note.split('\n'): + w_std(txt_wrpr.fill(para)) + txt_wrpr.width += 8 + txt_wrpr.subsequent_indent = old_si + txt_wrpr.initial_indent = old_ii + +def _print_list(alist, title): + """Print a list.""" + # TP: used in e.g. 'Existing alias addresses' or 'Existing accounts' + msg = u'%s %s' % (_(u'Existing'), title) + w_std(msg, u'-' * len(msg)) + if alist: + if title != _(u'alias domains'): + w_std(*(u'\t%s' % item for item in alist)) + else: + for domain in alist: + if not domain.startswith('xn--'): + w_std(u'\t%s' % domain) + else: + w_std(u'\t%s (%s)' % (domain, domain.decode('idna'))) + print + else: + w_std(_(u'\tNone'), '') + + +def _print_aliase_info(alias, destinations): + """Print the alias address and all its destinations""" + title = _(u'Alias information') + w_std(title, u'-' * len(title)) + w_std(_(u'\tMail for %s will be redirected to:') % alias) + w_std(*(u'\t * %s' % dest for dest in destinations)) + print + + +def _print_catchall_info(domain, destinations): + """Print the catchall destinations of a domain""" + title = _(u'Catch-all information') + w_std(title, u'-' * len(title)) + w_std(_(u'\tMail to unknown localparts in domain %s will be sent to:') + % domain) + w_std(*(u'\t * %s' % dest for dest in destinations)) + print + + +def _print_relocated_info(**kwargs): + """Print the old and new addresses of a relocated user.""" + title = _(u'Relocated information') + w_std(title, u'-' * len(title)) + w_std(_(u"\tUser '%(addr)s' has moved to '%(dest)s'") % kwargs, '') + + +def _format_domain(domain, main=True): + """format (prefix/convert) the domain name.""" + if domain.startswith('xn--'): + domain = u'%s (%s)' % (domain, domain.decode('idna')) + if main: + return u'\t[+] %s' % domain + return u'\t[-] %s' % domain + + +def _print_domain_list(dids, domains, matching): + """Print a list of (matching) domains/alias domains.""" + if matching: + title = _(u'Matching domains') + else: + title = _(u'Existing domains') + w_std(title, '-' * len(title)) + if domains: + for did in dids: + if domains[did][0] is not None: + w_std(_format_domain(domains[did][0])) + if len(domains[did]) > 1: + w_std(*(_format_domain(a, False) for a in domains[did][1:])) + else: + w_std(_('\tNone')) + print + + +def _print_address_list(which, dids, addresses, matching): + """Print a list of (matching) addresses.""" + _trans = { TYPE_ACCOUNT : _('user accounts') + , TYPE_ALIAS : _('aliases') + , TYPE_RELOCATED : _('relocated entries') + , TYPE_ACCOUNT | TYPE_ALIAS + : _('user accounts and aliases') + , TYPE_ACCOUNT | TYPE_RELOCATED + : _('user accounts and relocated entries') + , TYPE_ALIAS | TYPE_RELOCATED + : _('aliases and relocated entries') + , TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED : _('addresses') + } + try: + if matching: + title = _(u'Matching %s') % _trans[which] + else: + title = _(u'Existing %s') % _trans[which] + w_std(title, '-' * len(title)) + except KeyError: + raise VMMError(_("Invalid address type for list: '%s'") % which, + INVALID_ARGUMENT) + if addresses: + if which & (which - 1) == 0: + # only one type is requested, so no type indicator + _trans = { TYPE_ACCOUNT : _('') + , TYPE_ALIAS : _('') + , TYPE_RELOCATED : _('') + } + else: + _trans = { TYPE_ACCOUNT : _('u') + , TYPE_ALIAS : _('a') + , TYPE_RELOCATED : _('r') + } + for did in dids: + for addr, atype, aliasdomain in addresses[did]: + if aliasdomain: + leader = '[%s-]' % _trans[atype] + else: + leader = '[%s+]' % _trans[atype] + w_std('\t%s %s' % (leader, addr)) + else: + w_std(_('\tNone')) + print + + +def _print_aliasdomain_info(info): + """Print alias domain information.""" + title = _(u'Alias domain information') + for key in ('alias', 'domain'): + if info[key].startswith('xn--'): + info[key] = u'%s (%s)' % (info[key], info[key].decode('idna')) + w_std(title, '-' * len(title), + _('\tThe alias domain %(alias)s belongs to:\n\t * %(domain)s') % + info, '') + +del _