VirtualMailManager/common.py
changeset 760 b678a1c43027
parent 748 659c4476c57c
child 761 e4e656f19771
equal deleted inserted replaced
748:659c4476c57c 760:b678a1c43027
     1 # -*- coding: UTF-8 -*-
       
     2 # Copyright (c) 2010 - 2014, Pascal Volk
       
     3 # See COPYING for distribution information.
       
     4 """
       
     5     VirtualMailManager.common
       
     6     ~~~~~~~~~~~~~~~~~~~~~~~~~
       
     7 
       
     8     Some common functions
       
     9 """
       
    10 
       
    11 import locale
       
    12 import os
       
    13 import re
       
    14 import stat
       
    15 
       
    16 from VirtualMailManager import ENCODING
       
    17 from VirtualMailManager.constants import INVALID_MAIL_LOCATION, \
       
    18      NOT_EXECUTABLE, NO_SUCH_BINARY, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED
       
    19 from VirtualMailManager.errors import VMMError
       
    20 
       
    21 VERSION_RE = re.compile(r'^(\d+)\.(\d+)\.(?:(\d+)|(alpha|beta|rc)(\d+))$')
       
    22 
       
    23 _version_level = dict(alpha=0xA, beta=0xB, rc=0xC)
       
    24 _version_cache = {}
       
    25 _ = lambda msg: msg
       
    26 
       
    27 
       
    28 def expand_path(path):
       
    29     """Expands paths, starting with ``.`` or ``~``, to an absolute path."""
       
    30     if path.startswith('.'):
       
    31         return os.path.abspath(path)
       
    32     if path.startswith('~'):
       
    33         return os.path.expanduser(path)
       
    34     return path
       
    35 
       
    36 
       
    37 def get_unicode(string):
       
    38     """Converts `string` to `unicode`, if necessary."""
       
    39     if isinstance(string, unicode):
       
    40         return string
       
    41     return unicode(string, ENCODING, 'replace')
       
    42 
       
    43 
       
    44 def lisdir(path):
       
    45     """Checks if `path` is a directory.  Doesn't follow symbolic links.
       
    46     Returns bool.
       
    47     """
       
    48     try:
       
    49         lstat = os.lstat(path)
       
    50     except OSError:
       
    51         return False
       
    52     return stat.S_ISDIR(lstat.st_mode)
       
    53 
       
    54 
       
    55 def exec_ok(binary):
       
    56     """Checks if the `binary` exists and if it is executable.
       
    57 
       
    58     Throws a `VMMError` if the `binary` isn't a file or is not
       
    59     executable.
       
    60     """
       
    61     binary = expand_path(binary)
       
    62     if not os.path.isfile(binary):
       
    63         raise VMMError(_(u"No such file: '%s'") % get_unicode(binary),
       
    64                        NO_SUCH_BINARY)
       
    65     if not os.access(binary, os.X_OK):
       
    66         raise VMMError(_(u"File is not executable: '%s'") %
       
    67                        get_unicode(binary), NOT_EXECUTABLE)
       
    68     return binary
       
    69 
       
    70 
       
    71 def human_size(size):
       
    72     """Converts the `size` in bytes in human readable format."""
       
    73     if not isinstance(size, (long, int)):
       
    74         try:
       
    75             size = long(size)
       
    76         except ValueError:
       
    77             raise TypeError("'size' must be a positive long or int.")
       
    78     if size < 0:
       
    79         raise ValueError("'size' must be a positive long or int.")
       
    80     if size < 1024:
       
    81         return str(size)
       
    82     # TP: abbreviations of gibibyte, tebibyte kibibyte and mebibyte
       
    83     prefix_multiply = ((_(u'TiB'), 1 << 40), (_(u'GiB'), 1 << 30),
       
    84                        (_(u'MiB'), 1 << 20), (_(u'KiB'), 1 << 10))
       
    85     for prefix, multiply in prefix_multiply:
       
    86         if size >= multiply:
       
    87             # TP: e.g.: '%(size)s %(prefix)s' -> '118.30 MiB'
       
    88             return _(u'%(size)s %(prefix)s') % {
       
    89                     'size': locale.format('%.2f', float(size) / multiply,
       
    90                                           True).decode(ENCODING, 'replace'),
       
    91                     'prefix': prefix}
       
    92 
       
    93 
       
    94 def size_in_bytes(size):
       
    95     """Converts the string `size` to a long (size in bytes).
       
    96 
       
    97     The string `size` can be suffixed with *b* (bytes), *k* (kilobytes),
       
    98     *M* (megabytes) or *G* (gigabytes).
       
    99     """
       
   100     if not isinstance(size, basestring) or not size:
       
   101         raise TypeError('size must be a non empty string.')
       
   102     if size[-1].upper() in ('B', 'K', 'M', 'G'):
       
   103         try:
       
   104             num = int(size[:-1])
       
   105         except ValueError:
       
   106             raise ValueError('Not a valid integer value: %r' % size[:-1])
       
   107         unit = size[-1].upper()
       
   108         if unit == 'B':
       
   109             return num
       
   110         elif unit == 'K':
       
   111             return num << 10L
       
   112         elif unit == 'M':
       
   113             return num << 20L
       
   114         else:
       
   115             return num << 30L
       
   116     else:
       
   117         try:
       
   118             num = int(size)
       
   119         except ValueError:
       
   120             raise ValueError('Not a valid size value: %r' % size)
       
   121         return num
       
   122 
       
   123 
       
   124 def validate_transport(transport, maillocation):
       
   125     """Checks if the `transport` is usable for the given `maillocation`.
       
   126 
       
   127     Throws a `VMMError` if the chosen `transport` is unable to write
       
   128     messages in the `maillocation`'s mailbox format.
       
   129 
       
   130     Arguments:
       
   131 
       
   132     `transport` : VirtualMailManager.transport.Transport
       
   133       a Transport object
       
   134     `maillocation` : VirtualMailManager.maillocation.MailLocation
       
   135       a MailLocation object
       
   136     """
       
   137     if transport.transport in ('virtual', 'virtual:') and \
       
   138       not maillocation.postfix:
       
   139         raise VMMError(_(u"Invalid transport '%(transport)s' for mailbox "
       
   140                          u"format '%(mbfmt)s'.") %
       
   141                        {'transport': transport.transport,
       
   142                         'mbfmt': maillocation.mbformat}, INVALID_MAIL_LOCATION)
       
   143 
       
   144 
       
   145 def version_hex(version_string):
       
   146     """Converts a Dovecot version, e.g.: '1.2.3' or '2.0.beta4', to an int.
       
   147     Raises a `ValueError` if the *version_string* has the wrong™ format.
       
   148 
       
   149     version_hex('1.2.3') -> 270548736
       
   150     hex(version_hex('1.2.3')) -> '0x10203f00'
       
   151     """
       
   152     global _version_cache
       
   153     if version_string in _version_cache:
       
   154         return _version_cache[version_string]
       
   155     version = 0
       
   156     version_mo = VERSION_RE.match(version_string)
       
   157     if not version_mo:
       
   158         raise ValueError('Invalid version string: %r' % version_string)
       
   159     major, minor, patch, level, serial = version_mo.groups()
       
   160     major = int(major)
       
   161     minor = int(minor)
       
   162     if patch:
       
   163         patch = int(patch)
       
   164     if serial:
       
   165         serial = int(serial)
       
   166 
       
   167     if major > 0xFF or minor > 0xFF or \
       
   168       patch and patch > 0xFF or serial and serial > 0xFF:
       
   169         raise ValueError('Invalid version string: %r' % version_string)
       
   170 
       
   171     version += major << 28
       
   172     version += minor << 20
       
   173     if patch:
       
   174         version += patch << 12
       
   175     version += _version_level.get(level, 0xF) << 8
       
   176     if serial:
       
   177         version += serial
       
   178 
       
   179     _version_cache[version_string] = version
       
   180     return version
       
   181 
       
   182 
       
   183 def version_str(version):
       
   184     """Converts a Dovecot version previously converted with version_hex back to
       
   185     a string.
       
   186     Raises a `TypeError` if *version* is not an int/long.
       
   187     Raises a `ValueError` if *version* is an incorrect int version.
       
   188     """
       
   189     global _version_cache
       
   190     if version in _version_cache:
       
   191         return _version_cache[version]
       
   192     if not isinstance(version, (int, long)):
       
   193         raise TypeError('Argument is not a int/long: %r', version)
       
   194     major = (version >> 28) & 0xFF
       
   195     minor = (version >> 20) & 0xFF
       
   196     patch = (version >> 12) & 0xFF
       
   197     level = (version >> 8) & 0x0F
       
   198     serial = version & 0xFF
       
   199 
       
   200     levels = dict(zip(_version_level.values(), _version_level.keys()))
       
   201     if level == 0xF and not serial:
       
   202         version_string = '%u.%u.%u' % (major, minor, patch)
       
   203     elif level in levels and not patch:
       
   204         version_string = '%u.%u.%s%u' % (major, minor, levels[level], serial)
       
   205     else:
       
   206         raise ValueError('Invalid version: %r' % hex(version))
       
   207 
       
   208     _version_cache[version] = version_string
       
   209     return version_string
       
   210 
       
   211 
       
   212 def format_domain_default(domaindata):
       
   213     """Format info output when the value displayed is the domain default."""
       
   214     # TP: [domain default] indicates that a user's setting is the same as
       
   215     # configured in the user's domain.
       
   216     # e.g.: [  0.84%] 42/5,000 [domain default]
       
   217     return _(u'%s [domain default]') % domaindata
       
   218 
       
   219 
       
   220 def search_addresses(dbh, typelimit=None, lpattern=None, llike=False,
       
   221                      dpattern=None, dlike=False):
       
   222     """'Search' for addresses by *pattern* in the database.
       
   223 
       
   224     The search is limited by *typelimit*, a bitfield with values TYPE_ACCOUNT,
       
   225     TYPE_ALIAS, TYPE_RELOCATED, or a bitwise OR thereof. If no limit is
       
   226     specified, all types will be searched.
       
   227 
       
   228     *lpattern* may be a local part or a partial local part - starting and/or
       
   229     ending with a '%' sign.  When the *lpattern* starts or ends with a '%' sign
       
   230     *llike* has to be `True` to perform a wildcard search. To retrieve all
       
   231     available addresses use the arguments' default values.
       
   232 
       
   233     *dpattern* and *dlike* behave analogously for the domain part of an
       
   234     address, allowing for separate pattern matching: testuser%@example.%
       
   235 
       
   236     The return value of this function is a tuple. The first element is a list
       
   237     of domain IDs sorted alphabetically by the corresponding domain names. The
       
   238     second element is a dictionary indexed by domain ID, holding lists to
       
   239     associated addresses. Each address is itself actually a tuple of address,
       
   240     type, and boolean indicating whether the address stems from an alias
       
   241     domain.
       
   242     """
       
   243     if typelimit is None:
       
   244             typelimit = TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED
       
   245     queries = []
       
   246     if typelimit & TYPE_ACCOUNT:
       
   247         queries.append('SELECT gid, local_part, %d AS type FROM users'
       
   248                        % TYPE_ACCOUNT)
       
   249     if typelimit & TYPE_ALIAS:
       
   250         queries.append('SELECT DISTINCT gid, address as local_part, '
       
   251                        '%d AS type FROM alias' % TYPE_ALIAS)
       
   252     if typelimit & TYPE_RELOCATED:
       
   253         queries.append('SELECT gid, address as local_part, %d AS type '
       
   254                        'FROM relocated' % TYPE_RELOCATED)
       
   255     sql = "SELECT gid, local_part || '@' || domainname AS address, "
       
   256     sql += 'type, NOT is_primary AS from_aliasdomain FROM ('
       
   257     sql += ' UNION '.join(queries)
       
   258     sql += ') a JOIN domain_name USING (gid)'
       
   259     nextkw = 'WHERE'
       
   260     sqlargs = []
       
   261     for like, field, pattern in ((dlike, 'domainname', dpattern),
       
   262                                  (llike, 'local_part', lpattern)):
       
   263         if like:
       
   264             match = 'LIKE'
       
   265         else:
       
   266             if not pattern:
       
   267                 continue
       
   268             match = '='
       
   269         sql += ' %s %s %s %%s' % (nextkw, field, match)
       
   270         sqlargs.append(pattern)
       
   271         nextkw = 'AND'
       
   272     sql += ' ORDER BY domainname, local_part'
       
   273     dbc = dbh.cursor()
       
   274     dbc.execute(sql, sqlargs)
       
   275     result = dbc.fetchall()
       
   276     dbc.close()
       
   277 
       
   278     gids = []
       
   279     daddrs = {}
       
   280     for gid, address, addrtype, aliasdomain in result:
       
   281         if gid not in daddrs:
       
   282             gids.append(gid)
       
   283             daddrs[gid] = []
       
   284         daddrs[gid].append((address, addrtype, aliasdomain))
       
   285     return gids, daddrs
       
   286 
       
   287 del _