# -*- coding: UTF-8 -*-# Copyright (c) 2007 - 2014, Pascal Volk# See COPYING for distribution information.""" VirtualMailManager.cli.subcommands ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ VirtualMailManager's cli subcommands."""importlocaleimportplatformfromargparseimportAction,ArgumentParser,ArgumentTypeError, \RawDescriptionHelpFormatterfromtextwrapimportTextWrapperfromtimeimportstrftime,strptimefromVirtualMailManagerimportENCODINGfromVirtualMailManager.cliimportget_winsize,w_err,w_stdfromVirtualMailManager.commonimporthuman_size,size_in_bytes, \version_str,format_domain_defaultfromVirtualMailManager.constantsimport__copyright__,__date__, \__version__,ACCOUNT_EXISTS,ALIAS_EXISTS,ALIASDOMAIN_ISDOMAIN, \DOMAIN_ALIAS_EXISTS,INVALID_ARGUMENT,RELOCATED_EXISTS,TYPE_ACCOUNT, \TYPE_ALIAS,TYPE_RELOCATEDfromVirtualMailManager.errorsimportVMMErrorfromVirtualMailManager.passwordimportlist_schemesfromVirtualMailManager.servicesetimportSERVICES__all__=('RunContext','alias_add','alias_delete','alias_info','aliasdomain_add','aliasdomain_delete','aliasdomain_info','aliasdomain_switch','catchall_add','catchall_delete','catchall_info','config_get','config_set','configure','domain_add','domain_delete','domain_info','domain_note','domain_quota','domain_services','domain_transport','get_user','list_addresses','list_aliases','list_domains','list_pwschemes','list_relocated','list_users','relocated_add','relocated_delete','relocated_info','setup_parser','user_add','user_delete','user_info','user_name','user_note','user_password','user_quota','user_services','user_transport',)WS_ROWS=get_winsize()[1]-2_=lambdamsg:msgtxt_wrpr=TextWrapper(width=WS_ROWS)classRunContext(object):"""Contains all information necessary to run a subcommand."""__slots__=('args','cget','hdlr')plan_a_b=_('Plan A failed ... trying Plan B: %(subcommand)s%(object)s')def__init__(self,args,handler):"""Create a new RunContext"""self.args=argsself.cget=handler.cfg_dgetself.hdlr=handlerdefalias_add(ctx):"""create a new alias e-mail address"""ctx.hdlr.alias_add(ctx.args.address.lower(),*ctx.args.destination)defalias_delete(ctx):"""delete the specified alias e-mail address or one of its destinations"""destination=ctx.args.destinationifctx.args.destinationelseNonectx.hdlr.alias_delete(ctx.args.address.lower(),destination)defalias_info(ctx):"""show the destination(s) of the specified alias"""address=ctx.args.address.lower()try:_print_aliase_info(address,ctx.hdlr.alias_info(address))exceptVMMErroraserr:iferr.codeisACCOUNT_EXISTS:w_err(0,ctx.plan_a_b%{'subcommand':'userinfo','object':address})ctx.args.scmd='userinfo'ctx.args.details=Noneuser_info(ctx)eliferr.codeisRELOCATED_EXISTS:w_err(0,ctx.plan_a_b%{'subcommand':'relocatedinfo','object':address})ctx.args.scmd='relocatedinfo'relocated_info(ctx)else:raisedefaliasdomain_add(ctx):"""create a new alias for an existing domain"""ctx.hdlr.aliasdomain_add(ctx.args.fqdn.lower(),ctx.args.destination.lower())defaliasdomain_delete(ctx):"""delete the specified alias domain"""ctx.hdlr.aliasdomain_delete(ctx.args.fqdn.lower())defaliasdomain_info(ctx):"""show the destination of the given alias domain"""fqdn=ctx.args.fqdn.lower()try:_print_aliasdomain_info(ctx.hdlr.aliasdomain_info(fqdn))exceptVMMErroraserr:iferr.codeisALIASDOMAIN_ISDOMAIN:w_err(0,ctx.plan_a_b%{'subcommand':'domaininfo','object':fqdn})ctx.args.scmd='domaininfo'domain_info(ctx)else:raisedefaliasdomain_switch(ctx):"""assign the given alias domain to an other domain"""ctx.hdlr.aliasdomain_switch(ctx.args.fqdn.lower(),ctx.args.destination.lower())defcatchall_add(ctx):"""create a new catchall alias e-mail address"""ctx.hdlr.catchall_add(ctx.args.fqdn.lower(),*ctx.args.destination)defcatchall_delete(ctx):"""delete the specified destination or all of the catchall destination"""destination=ctx.args.destinationifctx.args.destinationelseNonectx.hdlr.catchall_delete(ctx.args.fqdn.lower(),destination)defcatchall_info(ctx):"""show the catchall destination(s) of the specified domain"""address=ctx.args.fqdn.lower()_print_catchall_info(address,ctx.hdlr.catchall_info(address))defconfig_get(ctx):"""show the actual value of the configuration option"""noop=lambdaoption:optionopt_formater={'misc.dovecot_version':version_str,'domain.quota_bytes':human_size,}option=ctx.args.option.lower()w_std('%s = %s'%(option,opt_formater.get(option,noop)(ctx.cget(option))))defconfig_set(ctx):"""set a new value for the configuration option"""ctx.hdlr.cfg_set(ctx.args.option.lower(),ctx.args.value)defconfigure(ctx):"""start interactive configuration mode"""ctx.hdlr.configure(ctx.args.section)defdomain_add(ctx):"""create a new domain"""fqdn=ctx.args.fqdn.lower()transport=ctx.args.transport.lower()ifctx.args.transportelseNonectx.hdlr.domain_add(fqdn,transport,ctx.args.note)ifctx.cget('domain.auto_postmaster'):w_std(_('Creating account for postmaster@%s')%fqdn)ctx.args.scmd='useradd'ctx.args.address='postmaster@%s'%fqdnctx.args.password=Nonectx.args.note=Noneuser_add(ctx)defdomain_delete(ctx):"""delete the given domain and all its alias domains"""ctx.hdlr.domain_delete(ctx.args.fqdn.lower(),ctx.args.force)defdomain_info(ctx):"""display information about the given domain"""fqdn=ctx.args.fqdn.lower()details=ctx.args.detailstry:info=ctx.hdlr.domain_info(fqdn,details)exceptVMMErroraserr:iferr.codeisDOMAIN_ALIAS_EXISTS:w_err(0,ctx.plan_a_b%{'subcommand':'aliasdomaininfo','object':fqdn})ctx.args.scmd='aliasdomaininfo'aliasdomain_info(ctx)else:raiseelse:q_limit='Storage: %(bytes)s; Messages: %(messages)s'ifnotdetails: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,_('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],_('Domain'))ifdetails=='accounts':_print_list(info[1],_('accounts'))elifdetails=='aliasdomains':_print_list(info[1],_('alias domains'))elifdetails=='aliases':_print_list(info[1],_('aliases'))elifdetails=='relocated':_print_list(info[1],_('relocated users'))elifdetails=='catchall':_print_list(info[1],_('catch-all destinations'))else:_print_list(info[1],_('alias domains'))_print_list(info[2],_('accounts'))_print_list(info[3],_('aliases'))_print_list(info[4],_('relocated users'))_print_list(info[5],_('catch-all destinations'))defdomain_quota(ctx):"""update the quota limit of the specified domain"""ctx.hdlr.domain_quotalimit(ctx.args.fqdn.lower(),ctx.args.storage,ctx.args.messages,ctx.args.force)defdomain_services(ctx):"""allow all named service and block the uncredited."""services=ctx.args.servicesifctx.args.serviceselse[]ctx.hdlr.domain_services(ctx.args.fqdn.lower(),ctx.args.force,*services)defdomain_transport(ctx):"""update the transport of the specified domain"""ctx.hdlr.domain_transport(ctx.args.fqdn.lower(),ctx.args.transport.lower(),ctx.args.force)defdomain_note(ctx):"""update the note of the given domain"""ctx.hdlr.domain_note(ctx.args.fqdn.lower(),ctx.args.note)defget_user(ctx):"""get the address of the user with the given UID"""_print_info(ctx,ctx.hdlr.user_by_uid(ctx.args.uid),_('Account'))deflist_domains(ctx):"""list all domains / search domains by pattern"""matching=Trueifctx.args.patternelseFalsepattern=ctx.args.pattern.lower()ifmatchingelseNonegids,domains=ctx.hdlr.domain_list(pattern)_print_domain_list(gids,domains,matching)deflist_pwschemes(ctx_unused):"""Prints all usable password schemes and password encoding suffixes."""keys=(_('Usable password schemes'),_('Usable encoding suffixes'))old_ii,old_si=txt_wrpr.initial_indent,txt_wrpr.subsequent_indenttxt_wrpr.initial_indent=txt_wrpr.subsequent_indent='\t'txt_wrpr.width=txt_wrpr.width-8forkey,valueinzip(keys,list_schemes()):w_std(key,len(key)*'-')w_std('\n'.join(txt_wrpr.wrap(' '.join(sorted(value)))),'')txt_wrpr.initial_indent,txt_wrpr.subsequent_indent=old_ii,old_sitxt_wrpr.width=txt_wrpr.width+8deflist_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."""iflimitisNone:limit=TYPE_ACCOUNT|TYPE_ALIAS|TYPE_RELOCATEDmatching=Trueifctx.args.patternelseFalsepattern=ctx.args.pattern.lower()ifmatchingelseNonegids,addresses=ctx.hdlr.address_list(limit,pattern)_print_address_list(limit,gids,addresses,matching)deflist_users(ctx):"""list all user accounts / search user accounts by pattern"""returnlist_addresses(ctx,TYPE_ACCOUNT)deflist_aliases(ctx):"""list all aliases / search aliases by pattern"""returnlist_addresses(ctx,TYPE_ALIAS)deflist_relocated(ctx):"""list all relocated records / search relocated records by pattern"""returnlist_addresses(ctx,TYPE_RELOCATED)defrelocated_add(ctx):"""create a new record for a relocated user"""ctx.hdlr.relocated_add(ctx.args.address.lower(),ctx.args.newaddress)defrelocated_delete(ctx):"""delete the record of the relocated user"""ctx.hdlr.relocated_delete(ctx.args.address.lower())defrelocated_info(ctx):"""print information about a relocated user"""relocated=ctx.args.address.lower()try:_print_relocated_info(addr=relocated,dest=ctx.hdlr.relocated_info(relocated))exceptVMMErroraserr:iferr.codeisACCOUNT_EXISTS:w_err(0,ctx.plan_a_b%{'subcommand':'userinfo','object':relocated})ctx.args.scmd='userinfo'ctx.args.details=Noneuser_info(ctx)eliferr.codeisALIAS_EXISTS:w_err(0,ctx.plan_a_b%{'subcommand':'aliasinfo','object':relocated})ctx.args.scmd='aliasinfo'alias_info(ctx)else:raisedefuser_add(ctx):"""create a new e-mail user with the given address"""gen_pass=ctx.hdlr.user_add(ctx.args.address.lower(),ctx.args.password,ctx.args.note)ifnotctx.args.passwordandgen_pass:w_std(_("Generated password: %s")%gen_pass)defuser_delete(ctx):"""delete the specified user"""ctx.hdlr.user_delete(ctx.args.address.lower(),ctx.args.force)defuser_info(ctx):"""display information about the given address"""address=ctx.args.address.lower()try:info=ctx.hdlr.user_info(address,ctx.args.details)exceptVMMErroraserr:iferr.codeisALIAS_EXISTS:w_err(0,ctx.plan_a_b%{'subcommand':'aliasinfo','object':address})ctx.args.scmd='aliasinfo'alias_info(ctx)eliferr.codeisRELOCATED_EXISTS:w_err(0,ctx.plan_a_b%{'subcommand':'relocatedinfo','object':address})ctx.args.scmd='relocatedinfo'relocated_info(ctx)else:raiseelse:ifctx.args.detailsin(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,_('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],_('Account'))_print_list(info[1],_('alias addresses'))defuser_name(ctx):"""set or update the real name for an address"""ctx.hdlr.user_name(ctx.args.address.lower(),ctx.args.name)defuser_password(ctx):"""update the password for the given address"""ctx.hdlr.user_password(ctx.args.address.lower(),ctx.args.password)defuser_note(ctx):"""update the note of the given address"""ctx.hdlr.user_note(ctx.args.address.lower(),ctx.args.note)defuser_quota(ctx):"""update the quota limit for the given address"""ctx.hdlr.user_quotalimit(ctx.args.address.lower(),ctx.args.storage,ctx.args.messages)defuser_services(ctx):"""allow all named service and block the uncredited."""if'domain'inctx.args.services:services=['domain']else:services=ctx.args.servicesctx.hdlr.user_services(ctx.args.address.lower(),*services)defuser_transport(ctx):"""update the transport of the given address"""ctx.hdlr.user_transport(ctx.args.address.lower(),ctx.args.transport)defsetup_parser():"""Create the argument parser, add all the subcommands and return it."""classArgParser(ArgumentParser):"""This class fixes the 'width detection'."""def_get_formatter(self):returnself.formatter_class(prog=self.prog,width=WS_ROWS,max_help_position=26)classVersionAction(Action):"""Show version and copyright information."""def__call__(self,parser,namespace,values,option_string=None):"""implements the Action API."""vers_info=_('{program}, version {version} (from {rel_date})\n''Python {py_vers} on {sysname}'.format(program=parser.prog,version=__version__,rel_date=strftime(locale.nl_langinfo(locale.D_FMT),strptime(__date__,'%Y-%m-%d')),py_vers=platform.python_version(),sysname=platform.system()))copy_info=_('{copyright}\n{program} is free software and comes ''with ABSOLUTELY NO WARRANTY.'.format(copyright=__copyright__,program=parser.prog))parser.exit(message='\n\n'.join((vers_info,copy_info))+'\n')defquota_storage(string):ifstring=='domain':returnstringtry:storage=size_in_bytes(string)except(TypeError,ValueError)aserror:raiseArgumentTypeError(str(error))returnstorageold_rw=txt_wrpr.replace_whitespacetxt_wrpr.replace_whitespace=Falsefill=lambdat:'\n'.join(txt_wrpr.fill(l)forlint.splitlines(True))mklst=lambdaiterable:'\n\t - '+'\n\t - '.join(iterable)description=_('%(prog)s - command line tool to manage email ''domains/accounts/aliases/...')epilog=_('use "%(prog)s <subcommand> -h" for information about the ''given subcommand')parser=ArgParser(description=description,epilog=epilog)parser.add_argument('-v','--version',action=VersionAction,nargs=0,help=_("show %(prog)s's version and copyright ""information and exit"))subparsers=parser.add_subparsers(metavar=_('<subcommand>'),title=_('list of available subcommands'))a=subparsers.add_parser#### general subcommands###cg=a('configget',aliases=('cg',),help=_('show the actual value of the configuration option'),epilog=_("This subcommand is used to display the actual value of ""the given configuration option."))cg.add_argument('option',help=_('the name of a configuration option'))cg.set_defaults(func=config_get,scmd='configget')cs=a('configset',aliases=('cs',),help=_('set a new value for the configuration option'),epilog=fill(_("Use this subcommand to set or update a single ""configuration option's value. option is the configuration ""option, value is the option's new value.\n\nNote: This ""subcommand will create a new vmm.cfg without any comments. ""Your current configuration file will be backed as ""vmm.cfg.bak.")),formatter_class=RawDescriptionHelpFormatter)cs.add_argument('option',help=_('the name of a configuration option'))cs.add_argument('value',help=_("the option's new value"))cs.set_defaults(func=config_set,scmd='configset')sections=('account','bin','database','domain','mailbox','misc')cf=a('configure',aliases=('cf',),help=_('start interactive configuration mode'),epilog=fill(_("Starts the interactive configuration for all ""configuration sections.\n\nIn this process the currently set ""value of each option will be displayed in square brackets. ""If no value is configured, the default value of each option ""will be displayed in square brackets. Press the return key, ""to accept the displayed value.\n\n""If the optional argument section is given, only the ""configuration options from the given section will be ""displayed and will be configurable. The following sections ""are available:\n")+mklst(sections)),formatter_class=RawDescriptionHelpFormatter)cf.add_argument('-s',choices=sections,metavar='SECTION',dest='section',help=_("configure only options of the given section"))cf.set_defaults(func=configure,scmd='configure')gu=a('getuser',aliases=('gu',),help=_('get the address of the user with the given UID'),epilog=_("If only the uid is available, for example from process ""list, the subcommand getuser will show the user's ""address."))gu.add_argument('uid',type=int,help=_("a user's unique identifier"))gu.set_defaults(func=get_user,scmd='getuser')ll=a('listaddresses',aliases=('ll',),help=_('list all addresses or search for addresses by pattern'),epilog=fill(_("This command lists all defined addresses. ""Addresses belonging to alias-domains are prefixed with a '-', ""addresses of regular domains with a '+'. Additionally, the ""letters 'u', 'a', and 'r' indicate the type of each address: ""user, alias and relocated respectively. The output can be ""limited with an optional pattern.\n\nTo perform a wild card ""search, the % character can be used at the start and/or the ""end of the pattern.")),formatter_class=RawDescriptionHelpFormatter)ll.add_argument('-p',help=_("the pattern to search for"),metavar='PATTERN',dest='pattern')ll.set_defaults(func=list_addresses,scmd='listaddresses')la=a('listaliases',aliases=('la',),help=_('list all aliases or search for aliases by pattern'),epilog=fill(_("This command lists all defined aliases. Aliases ""belonging to alias-domains are prefixed with a '-', addresses ""of regular domains with a '+'. The output can be limited ""with an optional pattern.\n\nTo perform a wild card search, ""the % character can be used at the start and/or the end of ""the pattern.")),formatter_class=RawDescriptionHelpFormatter)la.add_argument('-p',help=_("the pattern to search for"),metavar='PATTERN',dest='pattern')la.set_defaults(func=list_aliases,scmd='listaliases')ld=a('listdomains',aliases=('ld',),help=_('list all domains or search for domains by pattern'),epilog=fill(_("This subcommand lists all available domains. All ""domain names will be prefixed either with `[+]', if the ""domain is a primary domain, or with `[-]', if it is an alias ""domain name. The output can be limited with an optional ""pattern.\n\nTo perform a wild card search, the % character ""can be used at the start and/or the end of the pattern.")),formatter_class=RawDescriptionHelpFormatter)ld.add_argument('-p',help=_("the pattern to search for"),metavar='PATTERN',dest='pattern')ld.set_defaults(func=list_domains,scmd='listdomains')lr=a('listrelocated',aliases=('lr',),help=_('list all relocated users or search for relocated users by ''pattern'),epilog=fill(_("This command lists all defined relocated addresses. ""Relocated entries belonging to alias-domains are prefixed ""with a '-', addresses of regular domains with a '+'. The ""output can be limited with an optional pattern.\n\nTo ""perform a wild card search, the % character can be used at ""the start and/or the end of the pattern.")),formatter_class=RawDescriptionHelpFormatter)lr.add_argument('-p',help=_("the pattern to search for"),metavar='PATTERN',dest='pattern')lr.set_defaults(func=list_relocated,scmd='listrelocated')lu=a('listusers',aliases=('lu',),help=_('list all user accounts or search for accounts by pattern'),epilog=fill(_("This command lists all user accounts. User accounts ""belonging to alias-domains are prefixed with a '-', ""addresses of regular domains with a '+'. The output can be ""limited with an optional pattern.\n\nTo perform a wild card ""search, the % character can be used at the start and/or the ""end of the pattern.")),formatter_class=RawDescriptionHelpFormatter)lu.add_argument('-p',help=_("the pattern to search for"),metavar='PATTERN',dest='pattern')lu.set_defaults(func=list_users,scmd='listusers')lp=a('listpwschemes',aliases=('lp',),help=_('lists all usable password schemes and password encoding ''suffixes'),epilog=fill(_("This subcommand lists all password schemes which ""could be used in the vmm.cfg as value of the ""misc.password_scheme option. The output varies, depending ""on the used Dovecot version and the system's libc.\n""Additionally a few usable encoding suffixes will be ""displayed. One of them can be appended to the password ""scheme.")),formatter_class=RawDescriptionHelpFormatter)lp.set_defaults(func=list_pwschemes,scmd='listpwschemes')#### domain subcommands###da=a('domainadd',aliases=('da',),help=_('create a new domain'),epilog=fill(_("Adds the new domain into the database and creates ""the domain directory.\n\nIf the optional argument transport ""is given, it will override the default transport ""(domain.transport) from vmm.cfg. The specified transport ""will be the default transport for all new accounts in this ""domain.")),formatter_class=RawDescriptionHelpFormatter)da.add_argument('fqdn',help=_('a fully qualified domain name'))da.add_argument('-n',metavar='NOTE',dest='note',help=_('the note that should be set'))da.add_argument('-t',metavar='TRANSPORT',dest='transport',help=_('a Postfix transport (transport: or ''transport:nexthop)'))da.set_defaults(func=domain_add,scmd='domainadd')details=('accounts','aliasdomains','aliases','catchall','relocated','full')di=a('domaininfo',aliases=('di',),help=_('display information about the given domain'),epilog=fill(_("This subcommand shows some information about the ""given domain.\n\nFor a more detailed information about the ""domain the optional argument details can be specified. A ""possible details value can be one of the following six ""keywords:\n")+mklst(details)),formatter_class=RawDescriptionHelpFormatter)di.add_argument('fqdn',help=_('a fully qualified domain name'))di.add_argument('-d',choices=details,dest='details',metavar='DETAILS',help=_('additionally details to display'))di.set_defaults(func=domain_info,scmd='domaininfo')do=a('domainnote',aliases=('do',),help=_('set, update or delete the note of the given domain'),epilog=_('With this subcommand, it is possible to attach a note to ''the specified domain. In order to delete an existing ''note, pass the -d option.'))do.add_argument('fqdn',help=_('a fully qualified domain name'))do_grp=do.add_mutually_exclusive_group(required=True)do_grp.add_argument('-d',action='store_true',dest='delete',help=_('delete the note, if any'))do_grp.add_argument('-n',metavar='NOTE',dest='note',help=_('the note that should be set'))do.set_defaults(func=domain_note,scmd='domainnote')dq=a('domainquota',aliases=('dq',),help=_('update the quota limit of the specified domain'),epilog=fill(_("This subcommand is used to configure a new quota ""limit for the accounts of the domain - not for the domain ""itself.\n\nThe default quota limit for accounts is defined ""in the vmm.cfg (domain.quota_bytes and ""domain.quota_messages).\n\nThe new quota limit will affect ""only those accounts for which the limit has not been ""overridden. If you want to restore the default to all ""accounts, you may pass the optional argument --force. When ""the argument messages was omitted the default number of ""messages 0 (zero) will be applied.")),formatter_class=RawDescriptionHelpFormatter)dq.add_argument('fqdn',help=_('a fully qualified domain name'))dq.add_argument('storage',type=quota_storage,help=_('quota limit in {kilo,mega,giga}bytes e.g. 2G ''or 2048M',))dq.add_argument('-m',default=0,type=int,metavar='MESSAGES',dest='messages',help=_('quota limit in number of messages (default: 0)'))dq.add_argument('--force',action='store_true',help=_('enforce the limit for all accounts'))dq.set_defaults(func=domain_quota,scmd='domainquota')ds=a('domainservices',aliases=('ds',),help=_('enables the specified services and disables all not ''specified services of the given domain'),epilog=fill(_("To define which services could be used by the users ""of the domain — with the given fqdn — use this ""subcommand.\n\nEach specified service will be enabled/""usable. All other services will be deactivated/unusable. ""Possible service names are: imap, pop3, sieve and smtp.\nThe ""new service set will affect only those accounts for which ""the set has not been overridden. If you want to restore the ""default to all accounts, you may pass the option--force.")),formatter_class=RawDescriptionHelpFormatter)ds.add_argument('fqdn',help=_('a fully qualified domain name'))ds.add_argument('-s',choices=SERVICES,help=_('services which should be usable'),metavar='SERVICE',nargs='+',dest='services')ds.add_argument('--force',action='store_true',help=_('enforce the service set for all accounts'))ds.set_defaults(func=domain_services,scmd='domainservices')dt=a('domaintransport',aliases=('dt',),help=_('update the transport of the specified domain'),epilog=fill(_("A new transport for the indicated domain can be set ""with this subcommand.\n\nThe new transport will affect only ""those accounts for which the transport has not been ""overridden. If you want to restore the default to all ""accounts, you may give the option --force.")),formatter_class=RawDescriptionHelpFormatter)dt.add_argument('fqdn',help=_('a fully qualified domain name'))dt.add_argument('transport',help=_('a Postfix transport (transport: or ''transport:nexthop)'))dt.add_argument('--force',action='store_true',help=_('enforce the transport for all accounts'))dt.set_defaults(func=domain_transport,scmd='domaintransport')dd=a('domaindelete',aliases=('dd',),help=_('delete the given domain and all its alias domains'),epilog=fill(_("This subcommand deletes the domain specified by ""fqdn.\n\nIf there are accounts, aliases and/or relocated ""users assigned to the given domain, vmm will abort the ""requested operation and show an error message. If you know, ""what you are doing, you can specify the optional argument ""--force.\n\nIf you really always know what you are doing, ""edit your vmm.cfg and set the option domain.force_deletion ""to true.")),formatter_class=RawDescriptionHelpFormatter)dd.add_argument('fqdn',help=_('a fully qualified domain name'))dd.add_argument('--force',action='store_true',help=_('also delete all accounts, aliases and/or ''relocated users'))dd.set_defaults(func=domain_delete,scmd='domaindelete')#### alias domain subcommands###ada=a('aliasdomainadd',aliases=('ada',),help=_('create a new alias for an existing domain'),epilog=_('This subcommand adds the new alias domain (fqdn) to ''the destination domain that should be aliased.'))ada.add_argument('fqdn',help=_('a fully qualified domain name'))ada.add_argument('destination',help=_('the fqdn of the destination domain'))ada.set_defaults(func=aliasdomain_add,scmd='aliasdomainadd')adi=a('aliasdomaininfo',aliases=('adi',),help=_('show the destination of the given alias domain'),epilog=_('This subcommand shows to which domain the alias domain ''fqdn is assigned to.'))adi.add_argument('fqdn',help=_('a fully qualified domain name'))adi.set_defaults(func=aliasdomain_info,scmd='aliasdomaininfo')ads=a('aliasdomainswitch',aliases=('ads',),help=_('assign the given alias domain to an other domain'),epilog=_('If the destination of the existing alias domain fqdn ''should be switched to another destination use this ''subcommand.'))ads.add_argument('fqdn',help=_('a fully qualified domain name'))ads.add_argument('destination',help=_('the fqdn of the destination domain'))ads.set_defaults(func=aliasdomain_switch,scmd='aliasdomainswitch')add=a('aliasdomaindelete',aliases=('add',),help=_('delete the specified alias domain'),epilog=_('Use this subcommand if the alias domain fqdn should be ''removed.'))add.add_argument('fqdn',help=_('a fully qualified domain name'))add.set_defaults(func=aliasdomain_delete,scmd='aliasdomaindelete')#### account subcommands###ua=a('useradd',aliases=('ua',),help=_('create a new e-mail user with the given address'),epilog=fill(_('Use this subcommand to create a new e-mail account ''for the given address.\n\nIf the password is not provided, ''vmm will prompt for it interactively. When no password is ''provided and account.random_password is set to true, vmm ''will generate a random password and print it to stdout ''after the account has been created.')),formatter_class=RawDescriptionHelpFormatter)ua.add_argument('address',help=_("an account's e-mail address (local-part@fqdn)"))ua.add_argument('-n',metavar='NOTE',dest='note',help=_('the note that should be set'))ua.add_argument('-p',metavar='PASSWORD',dest='password',help=_("the new user's password"))ua.set_defaults(func=user_add,scmd='useradd')details=('aliases','du','full')ui=a('userinfo',aliases=('ui',),help=_('display information about the given address'),epilog=fill(_('This subcommand displays some information about ''the account specified by the given address.\n\nIf the ''optional argument details is given some more information ''will be displayed.\nPossible values for details are:\n')+mklst(details)),formatter_class=RawDescriptionHelpFormatter)ui.add_argument('address',help=_("an account's e-mail address (local-part@fqdn)"))ui.add_argument('-d',choices=details,metavar='DETAILS',dest='details',help=_('additionally details to display'))ui.set_defaults(func=user_info,scmd='userinfo')un=a('username',aliases=('un',),help=_('set, update or delete the real name for an address'),epilog=fill(_("The user's real name can be set/updated with this ""subcommand.\n\nIn order to delete the value stored for the ""account, pass the -d option.")),formatter_class=RawDescriptionHelpFormatter)un.add_argument('address',help=_("an account's e-mail address (local-part@fqdn)"))un_grp=un.add_mutually_exclusive_group(required=True)un_grp.add_argument('-d',action='store_true',dest='delete',help=_("delete the user's name if any"))un_grp.add_argument('-n',help=_("a user's real name"),metavar='NAME',dest='name')un.set_defaults(func=user_name,scmd='username')uo=a('usernote',aliases=('uo',),help=_('set, update or delete the note of the given address'),epilog=_('With this subcommand, it is possible to attach a note to ''the specified account. In order to delete an existing note, ''pass the -d option.'))uo.add_argument('address',help=_("an account's e-mail address (local-part@fqdn)"))uo_grp=uo.add_mutually_exclusive_group(required=True)uo_grp.add_argument('-d',action='store_true',dest='delete',help=_('delete the note, if any'))uo_grp.add_argument('-n',metavar='NOTE',dest='note',help=_('the note that should be set'))uo.set_defaults(func=user_note,scmd='usernote')up=a('userpassword',aliases=('up',),help=_('update the password for the given address'),epilog=fill(_("The password of an account can be updated with this ""subcommand.\n\nIf no password was provided, vmm will prompt ""for it interactively.")),formatter_class=RawDescriptionHelpFormatter)up.add_argument('address',help=_("an account's e-mail address (local-part@fqdn)"))up.add_argument('-p',metavar='PASSWORD',dest='password',help=_("the user's new password"))up.set_defaults(func=user_password,scmd='userpassword')uq=a('userquota',aliases=('uq',),help=_('update the quota limit for the given address'),epilog=fill(_("This subcommand is used to set a new quota limit ""for the given account.\n\nWhen the argument messages was ""omitted the default number of messages 0 (zero) will be ""applied.\n\nInstead of a storage limit pass the keyword ""'domain' to remove the account-specific override, causing ""the domain's value to be in effect.")),formatter_class=RawDescriptionHelpFormatter)uq.add_argument('address',help=_("an account's e-mail address (local-part@fqdn)"))uq.add_argument('storage',type=quota_storage,help=_('quota limit in {kilo,mega,giga}bytes e.g. 2G ''or 2048M'))uq.add_argument('-m',default=0,type=int,metavar='MESSAGES',dest='messages',help=_('quota limit in number of messages (default: 0)'))uq.set_defaults(func=user_quota,scmd='userquota')us=a('userservices',aliases=('us',),help=_('enable the specified services and disables all not ''specified services'),epilog=fill(_("To grant a user access to the specified service(s), ""use this command.\n\nAll omitted services will be ""deactivated/unusable for the user with the given ""address.\n\nInstead of any service pass the keyword ""'domain' to remove the account-specific override, causing ""the domain's value to be in effect.")),formatter_class=RawDescriptionHelpFormatter)us.add_argument('address',help=_("an account's e-mail address (local-part@fqdn)"))us.add_argument('-s',choices=SERVICES+('domain',),help=_('services which should be usable'),metavar='SERVICE',nargs='+',dest='services')us.set_defaults(func=user_services,scmd='userservices')ut=a('usertransport',aliases=('ut',),help=_('update the transport of the given address'),epilog=fill(_("A different transport for an account can be ""specified with this subcommand.\n\nInstead of a transport ""pass the keyword 'domain' to remove the account-specific ""override, causing the domain's value to be in effect.")),formatter_class=RawDescriptionHelpFormatter)ut.add_argument('address',help=_("an account's e-mail address (local-part@fqdn)"))ut.add_argument('transport',help=_('a Postfix transport (transport: or ''transport:nexthop)'))ut.set_defaults(func=user_transport,scmd='usertransport')ud=a('userdelete',aliases=('ud',),help=_('delete the specified user'),epilog=fill(_('Use this subcommand to delete the account with the ''given address.\n\nIf there are one or more aliases with an ''identical destination address, vmm will abort the requested ''operation and show an error message. To prevent this, ''give the optional argument --force.')),formatter_class=RawDescriptionHelpFormatter)ud.add_argument('address',help=_("an account's e-mail address (local-part@fqdn)"))ud.add_argument('--force',action='store_true',help=_('also delete assigned alias addresses'))ud.set_defaults(func=user_delete,scmd='userdelete')#### alias subcommands###aa=a('aliasadd',aliases=('aa',),help=_('create a new alias e-mail address with one or more ''destinations'),epilog=fill(_("This subcommand is used to create a new alias ""address with one or more destination addresses.\n\nWithin ""the destination address, the placeholders %n, %d, and %= ""will be replaced by the local part, the domain, or the ""email address with '@' replaced by '=' respectively. In ""combination with alias domains, this enables ""domain-specific destinations.")),formatter_class=RawDescriptionHelpFormatter)aa.add_argument('address',help=_("an alias' e-mail address (local-part@fqdn)"))aa.add_argument('destination',nargs='+',help=_("a destination's e-mail address (local-part@fqdn)"))aa.set_defaults(func=alias_add,scmd='aliasadd')ai=a('aliasinfo',aliases=('ai',),help=_('show the destination(s) of the specified alias'),epilog=_('Information about the alias with the given address can ''be displayed with this subcommand.'))ai.add_argument('address',help=_("an alias' e-mail address (local-part@fqdn)"))ai.set_defaults(func=alias_info,scmd='aliasinfo')ad=a('aliasdelete',aliases=('ad',),help=_('delete the specified alias e-mail address or one of its ''destinations'),epilog=fill(_("This subcommand is used to delete one or multiple ""destinations from the alias with the given address.\n\nWhen ""no destination address was specified the alias with all its ""destinations will be deleted.")),formatter_class=RawDescriptionHelpFormatter)ad.add_argument('address',help=_("an alias' e-mail address (local-part@fqdn)"))ad.add_argument('destination',nargs='*',help=_("a destination's e-mail address (local-part@fqdn)"))ad.set_defaults(func=alias_delete,scmd='aliasdelete')#### catch-all subcommands###caa=a('catchalladd',aliases=('caa',),help=_('add one or more catch-all destinations for a domain'),epilog=fill(_('This subcommand allows to specify destination ''addresses for a domain, which shall receive mail addressed ''to unknown local parts within that domain. Those catch-all ''aliases hence "catch all" mail to any address in the domain ''(unless a more specific alias, mailbox or relocated entry ''exists).\n\nWARNING: Catch-all addresses can cause mail ''server flooding because spammers like to deliver mail to ''all possible combinations of names, e.g. to all addresses ''between abba@example.org and zztop@example.org.')),formatter_class=RawDescriptionHelpFormatter)caa.add_argument('fqdn',help=_('a fully qualified domain name'))caa.add_argument('destination',nargs='+',help=_("a destination's e-mail address (local-part@fqdn)"))caa.set_defaults(func=catchall_add,scmd='catchalladd')cai=a('catchallinfo',aliases=('cai',),help=_('show the catch-all destination(s) of the specified ''domain'),epilog=_('This subcommand displays information about catch-all ''aliases defined for a domain.'))cai.add_argument('fqdn',help=_('a fully qualified domain name'))cai.set_defaults(func=catchall_info,scmd='catchallinfo')cad=a('catchalldelete',aliases=('cad',),help=_("delete the specified catch-all destination or all of a ""domain's destinations"),epilog=_('With this subcommand, catch-all aliases defined for a ''domain can be removed, either all of them, or those ''destinations which were specified explicitly.'))cad.add_argument('fqdn',help=_('a fully qualified domain name'))cad.add_argument('destination',nargs='*',help=_("a destination's e-mail address (local-part@fqdn)"))cad.set_defaults(func=catchall_delete,scmd='catchalldelete')#### relocated subcommands###ra=a('relocatedadd',aliases=('ra',),help=_('create a new record for a relocated user'),epilog=_("A new relocated user can be created with this ""subcommand."))ra.add_argument('address',help=_("a relocated user's e-mail address ""(local-part@fqdn)"))ra.add_argument('newaddress',help=_('e-mail address where the user can be reached now'))ra.set_defaults(func=relocated_add,scmd='relocatedadd')ri=a('relocatedinfo',aliases=('ri',),help=_('print information about a relocated user'),epilog=_('This subcommand shows the new address of the relocated ''user with the given address.'))ri.add_argument('address',help=_("a relocated user's e-mail address ""(local-part@fqdn)"))ri.set_defaults(func=relocated_info,scmd='relocatedinfo')rd=a('relocateddelete',aliases=('rd',),help=_('delete the record of the relocated user'),epilog=_('Use this subcommand in order to delete the relocated ''user with the given address.'))rd.add_argument('address',help=_("a relocated user's e-mail address ""(local-part@fqdn)"))rd.set_defaults(func=relocated_delete,scmd='relocateddelete')txt_wrpr.replace_whitespace=old_rwreturnparserdef_get_order(ctx):"""returns a tuple with (key, 1||0) tuples. Used by functions, which get a dict from the handler."""order=()ifctx.args.scmd=='domaininfo':order=(('domain name',0),('gid',1),('domain directory',0),('quota limit/user',0),('active services',0),('transport',0),('alias domains',0),('accounts',0),('aliases',0),('relocated',0),('catch-all dests',0))elifctx.args.scmd=='userinfo':ifctx.args.detailsin('du','full')or \ctx.cget('account.disk_usage'):order=(('address',0),('name',0),('uid',1),('gid',1),('home',0),('mail_location',0),('quota storage',0),('quota messages',0),('disk usage',0),('transport',0),('smtp',1),('pop3',1),('imap',1),('sieve',1))else:order=(('address',0),('name',0),('uid',1),('gid',1),('home',0),('mail_location',0),('quota storage',0),('quota messages',0),('transport',0),('smtp',1),('pop3',1),('imap',1),('sieve',1))elifctx.args.scmd=='getuser':order=(('uid',1),('gid',1),('address',0))returnorderdef_format_quota_usage(limit,used,human=False,domaindefault=False):"""Put quota's limit / usage / percentage in a formatted string."""ifhuman: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),}iflimit:q_usage['percent']=locale.format('%6.2f',100./limit*used,True)else:q_usage['percent']=locale.format('%6.2f',0,True)fmt=format_domain_defaultifdomaindefaultelselambdas:s# TP: e.g.: [ 0.00%] 21.09 KiB/1.00 GiBreturnfmt(_('[%(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='%s%s'%(title,_('information'))w_std(msg,'-'*len(msg))forkey,upperin_get_order(ctx):ifupper:w_std('\t%s: %s'%(key.upper().ljust(17,'.'),info[key]))else:w_std('\t%s: %s'%(key.title().ljust(17,'.'),info[key]))print()note=info.get('note')ifnote:_print_note(note+'\n')def_print_note(note):msg=_('Note')w_std(msg,'-'*len(msg))old_ii=txt_wrpr.initial_indentold_si=txt_wrpr.subsequent_indenttxt_wrpr.initial_indent=txt_wrpr.subsequent_indent='\t'txt_wrpr.width-=8forparainnote.split('\n'):w_std(txt_wrpr.fill(para))txt_wrpr.width+=8txt_wrpr.subsequent_indent=old_sitxt_wrpr.initial_indent=old_iidef_print_list(alist,title):"""Print a list."""# TP: used in e.g. 'Existing alias addresses' or 'Existing accounts'msg='%s%s'%(_('Existing'),title)w_std(msg,'-'*len(msg))ifalist:iftitle!=_('alias domains'):w_std(*('\t%s'%itemforiteminalist))else:fordomaininalist:ifnotdomain.startswith('xn--'):w_std('\t%s'%domain)else:w_std('\t%s (%s)'%(domain,domain.encode('utf-8').decode('idna')))print()else:w_std(_('\tNone'),'')def_print_aliase_info(alias,destinations):"""Print the alias address and all its destinations"""title=_('Alias information')w_std(title,'-'*len(title))w_std(_('\tMail for %s will be redirected to:')%alias)w_std(*('\t * %s'%destfordestindestinations))print()def_print_catchall_info(domain,destinations):"""Print the catchall destinations of a domain"""title=_('Catch-all information')w_std(title,'-'*len(title))w_std(_('\tMail to unknown local-parts in domain %s will be sent to:')%domain)w_std(*('\t * %s'%destfordestindestinations))print()def_print_relocated_info(**kwargs):"""Print the old and new addresses of a relocated user."""title=_('Relocated information')w_std(title,'-'*len(title))w_std(_("\tUser '%(addr)s' has moved to '%(dest)s'")%kwargs,'')def_format_domain(domain,main=True):"""format (prefix/convert) the domain name."""ifdomain.startswith('xn--'):domain='%s (%s)'%(domain,domain.encode('utf-8').decode('idna'))ifmain:return'\t[+] %s'%domainreturn'\t[-] %s'%domaindef_print_domain_list(dids,domains,matching):"""Print a list of (matching) domains/alias domains."""title=_('Matching domains')ifmatchingelse_('Existing domains')w_std(title,'-'*len(title))ifdomains:fordidindids:ifdomains[did][0]isnotNone:w_std(_format_domain(domains[did][0]))iflen(domains[did])>1:w_std(*(_format_domain(a,False)foraindomains[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 users'),TYPE_ACCOUNT|TYPE_ALIAS:_('user accounts and aliases'),TYPE_ACCOUNT|TYPE_RELOCATED:_('user accounts and relocated users'),TYPE_ALIAS|TYPE_RELOCATED:_('aliases and relocated users'),TYPE_ACCOUNT|TYPE_ALIAS|TYPE_RELOCATED:_('addresses'),}try:ifmatching:title=_('Matching %s')%_trans[which]else:title=_('Existing %s')%_trans[which]w_std(title,'-'*len(title))exceptKeyError:raiseVMMError(_("Invalid address type for list: '%s'")%which,INVALID_ARGUMENT)ifaddresses:ifwhich&(which-1)==0:# only one type is requested, so no type indicator_trans={TYPE_ACCOUNT:'',TYPE_ALIAS:'',TYPE_RELOCATED:''}else:# TP: the letters 'u', 'a' and 'r' are abbreviations of user,# alias and relocated user_trans={TYPE_ACCOUNT:_('u'),TYPE_ALIAS:_('a'),TYPE_RELOCATED:_('r'),}fordidindids:foraddr,atype,aliasdomaininaddresses[did]:ifaliasdomain: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=_('Alias domain information')forkeyin('alias','domain'):ifinfo[key].startswith('xn--'):info[key]='%s (%s)'%(info[key],info[key].encode(ENCODING).decode('idna'))w_std(title,'-'*len(title),_('\tThe alias domain %(alias)s belongs to:\n\t * %(domain)s')%info,'')del_