# HG changeset patch # User martin f. krafft # Date 1334488641 -7200 # Node ID 499c63f52462f13276a22ae48c5221bb9b141401 # Parent a93671970617dcdf3b6888112c526f0214f5be3d Provide list{addresses,aliases,users,relocated} subcommands The patch provides the list{addresses,aliases,users,relocated} subcommands to the UI. All commands actually take the same path in the code and only one query is run with different parameters for each case. There are still two shortcomings: 1. With alias domains, the output order is not as one might want it, e.g. foo@example.org bar@example.org foo@example.com bar@example.com when it should really be foo@ twice and then bar@ twice. I have not found a way to modify the SQL accordingly. 2. The SELECT queries for Accounts, Alias and Relocated are hard-coded in common.py. diff -r a93671970617 -r 499c63f52462 TODO --- a/TODO Sun Apr 15 19:56:21 2012 +0200 +++ b/TODO Sun Apr 15 13:17:21 2012 +0200 @@ -40,9 +40,3 @@ flexibility, there ought to be a m:n table connecting domains to sets of default aliases. These aliases then get treated like normal aliases unless they are overridden by a real alias. - -- list{aliases,accounts,relocated,addresses} subcommands [madduck@madduck.net] - - I would find it useful if I could query the database for all defined - users, aliases, relocated, and addresses (the union of all three). - Ideally, the commands would also take % wildcards, so that I can limit - the output diff -r a93671970617 -r 499c63f52462 VirtualMailManager/cli/handler.py --- a/VirtualMailManager/cli/handler.py Sun Apr 15 19:56:21 2012 +0200 +++ b/VirtualMailManager/cli/handler.py Sun Apr 15 13:17:21 2012 +0200 @@ -11,11 +11,11 @@ import os from VirtualMailManager.errors import VMMError -from VirtualMailManager.handler import Handler, TYPE_ACCOUNT +from VirtualMailManager.handler import Handler from VirtualMailManager.cli import read_pass from VirtualMailManager.cli.config import CliConfig as Cfg from VirtualMailManager.constants import ACCOUNT_EXISTS, INVALID_SECTION, \ - NO_SUCH_ACCOUNT + NO_SUCH_ACCOUNT, TYPE_ACCOUNT from VirtualMailManager.password import randompw _ = lambda msg: msg diff -r a93671970617 -r 499c63f52462 VirtualMailManager/cli/subcommands.py --- a/VirtualMailManager/cli/subcommands.py Sun Apr 15 19:56:21 2012 +0200 +++ b/VirtualMailManager/cli/subcommands.py Sun Apr 15 13:17:21 2012 +0200 @@ -20,7 +20,8 @@ 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 + 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 @@ -32,7 +33,8 @@ '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', 'relocated_add', 'relocated_delete', + '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', @@ -491,6 +493,34 @@ 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: @@ -811,8 +841,17 @@ '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', @@ -987,6 +1026,52 @@ 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') diff -r a93671970617 -r 499c63f52462 VirtualMailManager/common.py --- a/VirtualMailManager/common.py Sun Apr 15 19:56:21 2012 +0200 +++ b/VirtualMailManager/common.py Sun Apr 15 13:17:21 2012 +0200 @@ -14,10 +14,10 @@ import stat from VirtualMailManager import ENCODING -from VirtualMailManager.constants import NOT_EXECUTABLE, NO_SUCH_BINARY +from VirtualMailManager.constants import NOT_EXECUTABLE, NO_SUCH_BINARY, \ + TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED from VirtualMailManager.errors import VMMError - VERSION_RE = re.compile(r'^(\d+)\.(\d+)\.(?:(\d+)|(alpha|beta|rc)(\d+))$') _version_level = dict(alpha=0xA, beta=0xB, rc=0xC) @@ -190,4 +190,73 @@ """Format info output when the value displayed is the domain default.""" return _(u'%s [domain default]') % domaindata + +def search_addresses(dbh, typelimit=None, lpattern=None, llike=False, + dpattern=None, dlike=False): + """'Search' for addresses by *pattern* in the database. + + The search is limited by *typelimit*, a bitfield with values TYPE_ACCOUNT, + TYPE_ALIAS, TYPE_RELOCATED, or a bitwise OR thereof. If no limit is + specified, all types will be searched. + + *lpattern* may be a local part or a partial local part - starting and/or + ending with a '%' sign. When the *lpattern* starts or ends with a '%' sign + *llike* has to be `True` to perform a wildcard search. To retrieve all + available addresses use the arguments' default values. + + *dpattern* and *dlike* behave analogously for the domain part of an + address, allowing for separate pattern matching: testuser%@example.% + + The return value of this function is a tuple. The first element is a list + of domain IDs sorted alphabetically by the corresponding domain names. The + second element is a dictionary indexed by domain ID, holding lists to + associated addresses. Each address is itself actually a tuple of address, + type, and boolean indicating whether the address stems from an alias + domain. + """ + if typelimit == None: + typelimit = TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED + queries = [] + if typelimit & TYPE_ACCOUNT: + queries.append('SELECT gid, local_part, %d AS type FROM users' + % TYPE_ACCOUNT) + if typelimit & TYPE_ALIAS: + queries.append('SELECT gid, address as local_part, %d AS type ' + 'FROM alias' % TYPE_ALIAS) + if typelimit & TYPE_RELOCATED: + queries.append('SELECT gid, address as local_part, %d AS type ' + 'FROM relocated' % TYPE_RELOCATED) + sql = "SELECT gid, local_part || '@' || domainname AS address, " + sql += 'type, NOT is_primary AS from_aliasdomain FROM (' + sql += ' UNION '.join(queries) + sql += ') a JOIN domain_name USING (gid)' + nextkw = 'WHERE' + sqlargs = [] + for like, field, pattern in ((dlike, 'domainname', dpattern), + (llike, 'local_part', lpattern)): + if like: + match = 'LIKE' + else: + if not pattern: continue + match = '=' + sql += ' %s %s %s %%s' % (nextkw, field, match) + sqlargs.append(pattern) + nextkw = 'AND' + sql += ' ORDER BY domainname, local_part' + dbc = dbh.cursor() + dbc.execute(sql, sqlargs) + result = dbc.fetchall() + dbc.close() + + gids = [] + daddrs = {} + lastgid = None + for gid, address, addrtype, aliasdomain in result: + if gid != lastgid: + gids.append(gid) + lastgid = gid + daddrs[gid] = [] + daddrs[gid].append((address, addrtype, aliasdomain)) + return gids, daddrs + del _ diff -r a93671970617 -r 499c63f52462 VirtualMailManager/constants.py --- a/VirtualMailManager/constants.py Sun Apr 15 19:56:21 2012 +0200 +++ b/VirtualMailManager/constants.py Sun Apr 15 13:17:21 2012 +0200 @@ -81,3 +81,9 @@ UNKNOWN_SERVICE = 65 VMM_ERROR = 67 VMM_TOO_MANY_FAILURES = 68 + +# address types + +TYPE_ACCOUNT = 0x1 +TYPE_ALIAS = 0x2 +TYPE_RELOCATED = 0x4 diff -r a93671970617 -r 499c63f52462 VirtualMailManager/handler.py --- a/VirtualMailManager/handler.py Sun Apr 15 19:56:21 2012 +0200 +++ b/VirtualMailManager/handler.py Sun Apr 15 13:17:21 2012 +0200 @@ -30,10 +30,10 @@ FOUND_DOTS_IN_PATH, INVALID_ARGUMENT, MAILDIR_PERM_MISMATCH, \ NOT_EXECUTABLE, NO_SUCH_ACCOUNT, NO_SUCH_ALIAS, NO_SUCH_BINARY, \ NO_SUCH_DIRECTORY, NO_SUCH_RELOCATED, RELOCATED_EXISTS, UNKNOWN_SERVICE, \ - VMM_ERROR + VMM_ERROR, LOCALPART_INVALID, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED from VirtualMailManager.domain import Domain from VirtualMailManager.emailaddress import DestinationEmailAddress, \ - EmailAddress + EmailAddress, RE_LOCALPART from VirtualMailManager.errors import \ DomainError, NotRootError, PermissionError, VMMError from VirtualMailManager.mailbox import new as new_mailbox @@ -50,16 +50,12 @@ CFG_FILE = 'vmm.cfg' CFG_PATH = '/root:/usr/local/etc:/etc' RE_DOMAIN_SEARCH = """^[a-z0-9-\.]+$""" -TYPE_ACCOUNT = 0x1 -TYPE_ALIAS = 0x2 -TYPE_RELOCATED = 0x4 OTHER_TYPES = { TYPE_ACCOUNT: (_(u'an account'), ACCOUNT_EXISTS), TYPE_ALIAS: (_(u'an alias'), ALIAS_EXISTS), TYPE_RELOCATED: (_(u'a relocated user'), RELOCATED_EXISTS), } - class Handler(object): """Wrapper class to simplify the access on all the stuff from VirtualMailManager""" @@ -593,6 +589,46 @@ self._db_connect() return search(self._dbh, pattern=pattern, like=like) + def address_list(self, typelimit, pattern=None): + """TODO""" + llike = dlike = False + lpattern = dpattern = None + if pattern: + parts = pattern.split('@', 2) + if len(parts) == 2: + # The pattern includes '@', so let's treat the + # parts separately to allow for pattern search like %@domain.% + lpattern = parts[0] + llike = lpattern.startswith('%') or lpattern.endswith('%') + dpattern = parts[1] + dlike = dpattern.startswith('%') or dpattern.endswith('%') + + if llike: + checkp = lpattern.strip('%') + else: + checkp = lpattern + if len(checkp) > 0 and re.search(RE_LOCALPART, checkp): + raise VMMError(_(u"The pattern '%s' contains invalid " + u"characters.") % pattern, LOCALPART_INVALID) + else: + # else just match on domains + # (or should that be local part, I don't know…) + dpattern = parts[0] + dlike = dpattern.startswith('%') or dpattern.endswith('%') + + if dlike: + checkp = dpattern.strip('%') + else: + checkp = dpattern + if len(checkp) > 0 and not re.match(RE_DOMAIN_SEARCH, checkp): + raise VMMError(_(u"The pattern '%s' contains invalid " + u"characters.") % pattern, DOMAIN_INVALID) + self._db_connect() + from VirtualMailManager.common import search_addresses + return search_addresses(self._dbh, typelimit=typelimit, + lpattern=lpattern, llike=llike, + dpattern=dpattern, dlike=dlike) + def user_add(self, emailaddress, password): """Wrapper around Account.set_password() and Account.save().""" acc = self._get_account(emailaddress)