~ubuntu-branches/debian/wheezy/vmm/wheezy

« back to all changes in this revision

Viewing changes to .pc/60_Loosen_permission_checks_on_configfiles.patch/VirtualMailManager/handler.py

  • Committer: Package Import Robot
  • Author(s): martin f. krafft
  • Date: 2012-04-07 14:17:52 UTC
  • Revision ID: package-import@ubuntu.com-20120407141752-d0d5sp0q169d8641
Tags: 0.6.0-1
Initial release.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: UTF-8 -*-
 
2
# Copyright (c) 2007 - 2012, Pascal Volk
 
3
# See COPYING for distribution information.
 
4
"""
 
5
   VirtualMailManager.handler
 
6
   ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
7
 
 
8
   A wrapper class. It wraps round all other classes and does some
 
9
   dependencies checks.
 
10
 
 
11
   Additionally it communicates with the PostgreSQL database, creates
 
12
   or deletes directories of domains or users.
 
13
"""
 
14
 
 
15
import os
 
16
import re
 
17
 
 
18
from shutil import rmtree
 
19
from subprocess import Popen, PIPE
 
20
 
 
21
from VirtualMailManager.account import Account
 
22
from VirtualMailManager.alias import Alias
 
23
from VirtualMailManager.aliasdomain import AliasDomain
 
24
from VirtualMailManager.catchall import CatchallAlias
 
25
from VirtualMailManager.common import exec_ok, lisdir
 
26
from VirtualMailManager.config import Config as Cfg
 
27
from VirtualMailManager.constants import MIN_GID, MIN_UID, \
 
28
     ACCOUNT_EXISTS, ALIAS_EXISTS, CONF_NOFILE, CONF_NOPERM, CONF_WRONGPERM, \
 
29
     DATABASE_ERROR, DOMAINDIR_GROUP_MISMATCH, DOMAIN_INVALID, \
 
30
     FOUND_DOTS_IN_PATH, INVALID_ARGUMENT, MAILDIR_PERM_MISMATCH, \
 
31
     NOT_EXECUTABLE, NO_SUCH_ACCOUNT, NO_SUCH_ALIAS, NO_SUCH_BINARY, \
 
32
     NO_SUCH_DIRECTORY, NO_SUCH_RELOCATED, RELOCATED_EXISTS, UNKNOWN_SERVICE, \
 
33
     VMM_ERROR, LOCALPART_INVALID, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED
 
34
from VirtualMailManager.domain import Domain
 
35
from VirtualMailManager.emailaddress import DestinationEmailAddress, \
 
36
     EmailAddress, RE_LOCALPART
 
37
from VirtualMailManager.errors import \
 
38
     DomainError, NotRootError, PermissionError, VMMError
 
39
from VirtualMailManager.mailbox import new as new_mailbox
 
40
from VirtualMailManager.pycompat import all, any
 
41
from VirtualMailManager.quotalimit import QuotaLimit
 
42
from VirtualMailManager.relocated import Relocated
 
43
from VirtualMailManager.serviceset import ServiceSet, SERVICES
 
44
from VirtualMailManager.transport import Transport
 
45
 
 
46
 
 
47
_ = lambda msg: msg
 
48
_db_mod = None
 
49
 
 
50
CFG_FILE = 'vmm.cfg'
 
51
CFG_DB_FILE = 'vmm-db.cfg'
 
52
CFG_PATH = '/etc/vmm'
 
53
RE_DOMAIN_SEARCH = """^[a-z0-9-\.]+$"""
 
54
OTHER_TYPES = {
 
55
    TYPE_ACCOUNT: (_(u'an account'), ACCOUNT_EXISTS),
 
56
    TYPE_ALIAS: (_(u'an alias'), ALIAS_EXISTS),
 
57
    TYPE_RELOCATED: (_(u'a relocated user'), RELOCATED_EXISTS),
 
58
}
 
59
 
 
60
class Handler(object):
 
61
    """Wrapper class to simplify the access on all the stuff from
 
62
    VirtualMailManager"""
 
63
    __slots__ = ('_cfg', '_cfg_fname', '_db_connect', '_dbh', '_warnings')
 
64
 
 
65
    def __init__(self, skip_some_checks=False):
 
66
        """Creates a new Handler instance.
 
67
 
 
68
        ``skip_some_checks`` : bool
 
69
            When a derived class knows how to handle all checks this
 
70
            argument may be ``True``. By default it is ``False`` and
 
71
            all checks will be performed.
 
72
 
 
73
        Throws a NotRootError if your uid is greater 0.
 
74
        """
 
75
        self._cfg_fname = ''
 
76
        self._warnings = []
 
77
        self._cfg = None
 
78
        self._dbh = None
 
79
        self._db_connect = None
 
80
 
 
81
        if os.geteuid():
 
82
            raise NotRootError(_(u"You are not root.\n\tGood bye!\n"),
 
83
                               CONF_NOPERM)
 
84
 
 
85
        db = map(lambda p: os.path.join(p, CFG_DB_FILE), CFG_PATH.split(':'))
 
86
        if self._check_cfg_file():
 
87
            self._cfg = Cfg(self._cfg_fname, db)
 
88
            self._cfg.load()
 
89
        if not skip_some_checks:
 
90
            self._cfg.check()
 
91
            self._chkenv()
 
92
            self._set_db_connect()
 
93
 
 
94
    def _find_cfg_file(self):
 
95
        """Search the CFG_FILE in CFG_PATH.
 
96
        Raise a VMMError when no vmm.cfg could be found.
 
97
        """
 
98
        for path in CFG_PATH.split(':'):
 
99
            tmp = os.path.join(path, CFG_FILE)
 
100
            if os.path.isfile(tmp):
 
101
                self._cfg_fname = tmp
 
102
                break
 
103
        if not self._cfg_fname:
 
104
            raise VMMError(_(u"Could not find '%(cfg_file)s' in: "
 
105
                             u"'%(cfg_path)s'") % {'cfg_file': CFG_FILE,
 
106
                           'cfg_path': CFG_PATH}, CONF_NOFILE)
 
107
 
 
108
    def _check_cfg_file(self):
 
109
        """Checks the configuration file, returns bool"""
 
110
        self._find_cfg_file()
 
111
        fstat = os.stat(self._cfg_fname)
 
112
        fmode = int(oct(fstat.st_mode & 0777))
 
113
        if fmode % 100 and fstat.st_uid != fstat.st_gid or \
 
114
           fmode % 10 and fstat.st_uid == fstat.st_gid:
 
115
            # TP: Please keep the backticks around the command. `chmod 0600 …`
 
116
            raise PermissionError(_(u"wrong permissions for '%(file)s': "
 
117
                                    u"%(perms)s\n`chmod 0600 %(file)s` would "
 
118
                                    u"be great.") % {'file': self._cfg_fname,
 
119
                                  'perms': fmode}, CONF_WRONGPERM)
 
120
        else:
 
121
            return True
 
122
 
 
123
    def _chkenv(self):
 
124
        """Make sure our base_directory is a directory and that all
 
125
        required executables exists and are executable.
 
126
        If not, a VMMError will be raised"""
 
127
        dir_created = False
 
128
        basedir = self._cfg.dget('misc.base_directory')
 
129
        if not os.path.exists(basedir):
 
130
            old_umask = os.umask(0006)
 
131
            os.makedirs(basedir, 0771)
 
132
            os.chown(basedir, 0, 0)
 
133
            os.umask(old_umask)
 
134
            dir_created = True
 
135
        if not dir_created and not lisdir(basedir):
 
136
            raise VMMError(_(u"'%(path)s' is not a directory.\n(%(cfg_file)s: "
 
137
                             u"section 'misc', option 'base_directory')") %
 
138
                           {'path': basedir, 'cfg_file': self._cfg_fname},
 
139
                           NO_SUCH_DIRECTORY)
 
140
        for opt, val in self._cfg.items('bin'):
 
141
            try:
 
142
                exec_ok(val)
 
143
            except VMMError, err:
 
144
                if err.code in (NO_SUCH_BINARY, NOT_EXECUTABLE):
 
145
                    raise VMMError(err.msg + _(u"\n(%(cfg_file)s: section "
 
146
                                   u"'bin', option '%(option)s')") %
 
147
                                   {'cfg_file': self._cfg_fname,
 
148
                                    'option': opt}, err.code)
 
149
                else:
 
150
                    raise
 
151
 
 
152
    def _set_db_connect(self):
 
153
        """check which module to use and set self._db_connect"""
 
154
        global _db_mod
 
155
        if self._cfg.dget('database.module').lower() == 'psycopg2':
 
156
            try:
 
157
                _db_mod = __import__('psycopg2')
 
158
            except ImportError:
 
159
                raise VMMError(_(u"Unable to import database module '%s'.") %
 
160
                               'psycopg2', VMM_ERROR)
 
161
            self._db_connect = self._psycopg2_connect
 
162
        else:
 
163
            try:
 
164
                tmp = __import__('pyPgSQL', globals(), locals(), ['PgSQL'])
 
165
            except ImportError:
 
166
                raise VMMError(_(u"Unable to import database module '%s'.") %
 
167
                               'pyPgSQL', VMM_ERROR)
 
168
            _db_mod = tmp.PgSQL
 
169
            self._db_connect = self._pypgsql_connect
 
170
 
 
171
    def _pypgsql_connect(self):
 
172
        """Creates a pyPgSQL.PgSQL.connection instance."""
 
173
        if self._dbh is None or (isinstance(self._dbh, _db_mod.Connection) and
 
174
                                  not self._dbh._isOpen):
 
175
            try:
 
176
                self._dbh = _db_mod.connect(
 
177
                        database=self._cfg.dget('database.name'),
 
178
                        user=self._cfg.pget('database.user'),
 
179
                        host=self._cfg.dget('database.host'),
 
180
                        port=self._cfg.dget('database.port'),
 
181
                        password=self._cfg.pget('database.pass'),
 
182
                        client_encoding='utf8', unicode_results=True)
 
183
                dbc = self._dbh.cursor()
 
184
                dbc.execute("SET NAMES 'UTF8'")
 
185
                dbc.close()
 
186
            except _db_mod.libpq.DatabaseError, err:
 
187
                raise VMMError(str(err), DATABASE_ERROR)
 
188
 
 
189
    def _psycopg2_connect(self):
 
190
        """Return a new psycopg2 connection object."""
 
191
        if self._dbh is None or \
 
192
          (isinstance(self._dbh, _db_mod.extensions.connection) and
 
193
           self._dbh.closed):
 
194
            try:
 
195
                self._dbh = _db_mod.connect(
 
196
                        host=self._cfg.dget('database.host'),
 
197
                        sslmode=self._cfg.dget('database.sslmode'),
 
198
                        port=self._cfg.dget('database.port'),
 
199
                        database=self._cfg.dget('database.name'),
 
200
                        user=self._cfg.pget('database.user'),
 
201
                        password=self._cfg.pget('database.pass'))
 
202
                self._dbh.set_client_encoding('utf8')
 
203
                _db_mod.extensions.register_type(_db_mod.extensions.UNICODE)
 
204
                dbc = self._dbh.cursor()
 
205
                dbc.execute("SET NAMES 'UTF8'")
 
206
                dbc.close()
 
207
            except _db_mod.DatabaseError, err:
 
208
                raise VMMError(str(err), DATABASE_ERROR)
 
209
 
 
210
    def _chk_other_address_types(self, address, exclude):
 
211
        """Checks if the EmailAddress *address* is known as `TYPE_ACCOUNT`,
 
212
        `TYPE_ALIAS` or `TYPE_RELOCATED`, but not as the `TYPE_*` specified
 
213
        by *exclude*.  If the *address* is known as one of the `TYPE_*`s
 
214
        the according `TYPE_*` constant will be returned.  Otherwise 0 will
 
215
        be returned."""
 
216
        assert exclude in (TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED) and \
 
217
                isinstance(address, EmailAddress)
 
218
        if exclude is not TYPE_ACCOUNT:
 
219
            account = Account(self._dbh, address)
 
220
            if account:
 
221
                return TYPE_ACCOUNT
 
222
        if exclude is not TYPE_ALIAS:
 
223
            alias = Alias(self._dbh, address)
 
224
            if alias:
 
225
                return TYPE_ALIAS
 
226
        if exclude is not TYPE_RELOCATED:
 
227
            relocated = Relocated(self._dbh, address)
 
228
            if relocated:
 
229
                return TYPE_RELOCATED
 
230
        return 0
 
231
 
 
232
    def _is_other_address(self, address, exclude):
 
233
        """Checks if *address* is known for an Account (TYPE_ACCOUNT),
 
234
        Alias (TYPE_ALIAS) or Relocated (TYPE_RELOCATED), except for
 
235
        *exclude*.  Returns `False` if the address is not known for other
 
236
        types.
 
237
 
 
238
        Raises a `VMMError` if the address is known.
 
239
        """
 
240
        other = self._chk_other_address_types(address, exclude)
 
241
        if not other:
 
242
            return False
 
243
        # TP: %(a_type)s will be one of: 'an account', 'an alias' or
 
244
        # 'a relocated user'
 
245
        msg = _(u"There is already %(a_type)s with the address '%(address)s'.")
 
246
        raise VMMError(msg % {'a_type': OTHER_TYPES[other][0],
 
247
                              'address': address}, OTHER_TYPES[other][1])
 
248
 
 
249
    def _get_account(self, address):
 
250
        """Return an Account instances for the given address (str)."""
 
251
        address = EmailAddress(address)
 
252
        self._db_connect()
 
253
        return Account(self._dbh, address)
 
254
 
 
255
    def _get_alias(self, address):
 
256
        """Return an Alias instances for the given address (str)."""
 
257
        address = EmailAddress(address)
 
258
        self._db_connect()
 
259
        return Alias(self._dbh, address)
 
260
 
 
261
    def _get_catchall(self, domain):
 
262
        """Return a CatchallAlias instances for the given domain (str)."""
 
263
        self._db_connect()
 
264
        return CatchallAlias(self._dbh, domain)
 
265
 
 
266
    def _get_relocated(self, address):
 
267
        """Return a Relocated instances for the given address (str)."""
 
268
        address = EmailAddress(address)
 
269
        self._db_connect()
 
270
        return Relocated(self._dbh, address)
 
271
 
 
272
    def _get_domain(self, domainname):
 
273
        """Return a Domain instances for the given domain name (str)."""
 
274
        self._db_connect()
 
275
        return Domain(self._dbh, domainname)
 
276
 
 
277
    def _get_disk_usage(self, directory):
 
278
        """Estimate file space usage for the given directory.
 
279
 
 
280
        Arguments:
 
281
 
 
282
        `directory` : basestring
 
283
          The directory to summarize recursively disk usage for
 
284
        """
 
285
        if lisdir(directory):
 
286
            return Popen([self._cfg.dget('bin.du'), "-hs", directory],
 
287
                         stdout=PIPE).communicate()[0].split('\t')[0]
 
288
        else:
 
289
            self._warnings.append(_('No such directory: %s') % directory)
 
290
            return 0
 
291
 
 
292
    def _make_domain_dir(self, domain):
 
293
        """Create a directory for the `domain` and its accounts."""
 
294
        cwd = os.getcwd()
 
295
        hashdir, domdir = domain.directory.split(os.path.sep)[-2:]
 
296
        dir_created = False
 
297
        os.chdir(self._cfg.dget('misc.base_directory'))
 
298
        old_umask = os.umask(0022)
 
299
        if not os.path.exists(hashdir):
 
300
            os.mkdir(hashdir, 0711)
 
301
            os.chown(hashdir, 0, 0)
 
302
            dir_created = True
 
303
        if not dir_created and not lisdir(hashdir):
 
304
            raise VMMError(_(u"'%s' is not a directory.") % hashdir,
 
305
                           NO_SUCH_DIRECTORY)
 
306
        if os.path.exists(domain.directory):
 
307
            raise VMMError(_(u"The file/directory '%s' already exists.") %
 
308
                           domain.directory, VMM_ERROR)
 
309
        os.mkdir(os.path.join(hashdir, domdir),
 
310
                 self._cfg.dget('domain.directory_mode'))
 
311
        os.chown(domain.directory, 0, domain.gid)
 
312
        os.umask(old_umask)
 
313
        os.chdir(cwd)
 
314
 
 
315
    def _make_home(self, account):
 
316
        """Create a home directory for the new Account *account*."""
 
317
        domdir = account.domain.directory
 
318
        if not lisdir(domdir):
 
319
            self._make_domain_dir(account.domain)
 
320
        os.umask(0007)
 
321
        uid = account.uid
 
322
        os.chdir(domdir)
 
323
        os.mkdir('%s' % uid, self._cfg.dget('account.directory_mode'))
 
324
        os.chown('%s' % uid, uid, account.gid)
 
325
 
 
326
    def _make_account_dirs(self, account):
 
327
        """Create all necessary directories for the account."""
 
328
        oldpwd = os.getcwd()
 
329
        self._make_home(account)
 
330
        mailbox = new_mailbox(account)
 
331
        mailbox.create()
 
332
        folders = self._cfg.dget('mailbox.folders').split(':')
 
333
        if any(folders):
 
334
            bad = mailbox.add_boxes(folders,
 
335
                                    self._cfg.dget('mailbox.subscribe'))
 
336
            if bad:
 
337
                self._warnings.append(_(u"Skipped mailbox folders:") +
 
338
                                      '\n\t- ' + '\n\t- '.join(bad))
 
339
        os.chdir(oldpwd)
 
340
 
 
341
    def _delete_home(self, domdir, uid, gid):
 
342
        """Delete a user's home directory.
 
343
 
 
344
        Arguments:
 
345
 
 
346
        `domdir` : basestring
 
347
          The directory of the domain the user belongs to
 
348
          (commonly AccountObj.domain.directory)
 
349
        `uid` : int/long
 
350
          The user's UID (commonly AccountObj.uid)
 
351
        `gid` : int/long
 
352
          The user's GID (commonly AccountObj.gid)
 
353
        """
 
354
        assert all(isinstance(xid, (long, int)) for xid in (uid, gid)) and \
 
355
                isinstance(domdir, basestring)
 
356
        if uid < MIN_UID or gid < MIN_GID:
 
357
            raise VMMError(_(u"UID '%(uid)u' and/or GID '%(gid)u' are less "
 
358
                             u"than %(min_uid)u/%(min_gid)u.") % {'uid': uid,
 
359
                           'gid': gid, 'min_gid': MIN_GID, 'min_uid': MIN_UID},
 
360
                           MAILDIR_PERM_MISMATCH)
 
361
        if domdir.count('..'):
 
362
            raise VMMError(_(u'Found ".." in domain directory path: %s') %
 
363
                           domdir, FOUND_DOTS_IN_PATH)
 
364
        if not lisdir(domdir):
 
365
            raise VMMError(_(u"No such directory: %s") % domdir,
 
366
                           NO_SUCH_DIRECTORY)
 
367
        os.chdir(domdir)
 
368
        userdir = '%s' % uid
 
369
        if not lisdir(userdir):
 
370
            self._warnings.append(_(u"No such directory: %s") %
 
371
                                  os.path.join(domdir, userdir))
 
372
            return
 
373
        mdstat = os.lstat(userdir)
 
374
        if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
 
375
            raise VMMError(_(u'Detected owner/group mismatch in home '
 
376
                             u'directory.'), MAILDIR_PERM_MISMATCH)
 
377
        rmtree(userdir, ignore_errors=True)
 
378
 
 
379
    def _delete_domain_dir(self, domdir, gid):
 
380
        """Delete a domain's directory.
 
381
 
 
382
        Arguments:
 
383
 
 
384
        `domdir` : basestring
 
385
          The domain's directory (commonly DomainObj.directory)
 
386
        `gid` : int/long
 
387
          The domain's GID (commonly DomainObj.gid)
 
388
        """
 
389
        assert isinstance(domdir, basestring) and isinstance(gid, (long, int))
 
390
        if gid < MIN_GID:
 
391
            raise VMMError(_(u"GID '%(gid)u' is less than '%(min_gid)u'.") %
 
392
                           {'gid': gid, 'min_gid': MIN_GID},
 
393
                           DOMAINDIR_GROUP_MISMATCH)
 
394
        if domdir.count('..'):
 
395
            raise VMMError(_(u'Found ".." in domain directory path: %s') %
 
396
                           domdir, FOUND_DOTS_IN_PATH)
 
397
        if not lisdir(domdir):
 
398
            self._warnings.append(_('No such directory: %s') % domdir)
 
399
            return
 
400
        dirst = os.lstat(domdir)
 
401
        if dirst.st_gid != gid:
 
402
            raise VMMError(_(u'Detected group mismatch in domain directory: '
 
403
                             u'%s') % domdir, DOMAINDIR_GROUP_MISMATCH)
 
404
        rmtree(domdir, ignore_errors=True)
 
405
 
 
406
    def has_warnings(self):
 
407
        """Checks if warnings are present, returns bool."""
 
408
        return bool(len(self._warnings))
 
409
 
 
410
    def get_warnings(self):
 
411
        """Returns a list with all available warnings and resets all
 
412
        warnings.
 
413
        """
 
414
        ret_val = self._warnings[:]
 
415
        del self._warnings[:]
 
416
        return ret_val
 
417
 
 
418
    def cfg_dget(self, option):
 
419
        """Get the configured value of the *option* (section.option).
 
420
        When the option was not configured its default value will be
 
421
        returned."""
 
422
        return self._cfg.dget(option)
 
423
 
 
424
    def cfg_pget(self, option):
 
425
        """Get the configured value of the *option* (section.option)."""
 
426
        return self._cfg.pget(option)
 
427
 
 
428
    def cfg_install(self):
 
429
        """Installs the cfg_dget method as ``cfg_dget`` into the built-in
 
430
        namespace."""
 
431
        import __builtin__
 
432
        assert 'cfg_dget' not in __builtin__.__dict__
 
433
        __builtin__.__dict__['cfg_dget'] = self._cfg.dget
 
434
 
 
435
    def domain_add(self, domainname, transport=None):
 
436
        """Wrapper around Domain's set_quotalimit, set_transport and save."""
 
437
        dom = self._get_domain(domainname)
 
438
        if transport is None:
 
439
            dom.set_transport(Transport(self._dbh,
 
440
                              transport=self._cfg.dget('domain.transport')))
 
441
        else:
 
442
            dom.set_transport(Transport(self._dbh, transport=transport))
 
443
        dom.set_quotalimit(QuotaLimit(self._dbh,
 
444
                           bytes=long(self._cfg.dget('domain.quota_bytes')),
 
445
                           messages=self._cfg.dget('domain.quota_messages')))
 
446
        dom.set_serviceset(ServiceSet(self._dbh,
 
447
                                      imap=self._cfg.dget('domain.imap'),
 
448
                                      pop3=self._cfg.dget('domain.pop3'),
 
449
                                      sieve=self._cfg.dget('domain.sieve'),
 
450
                                      smtp=self._cfg.dget('domain.smtp')))
 
451
        dom.set_directory(self._cfg.dget('misc.base_directory'))
 
452
        dom.save()
 
453
        self._make_domain_dir(dom)
 
454
 
 
455
    def domain_quotalimit(self, domainname, bytes_, messages=0, force=None):
 
456
        """Wrapper around Domain.update_quotalimit()."""
 
457
        if not all(isinstance(i, (int, long)) for i in (bytes_, messages)):
 
458
            raise TypeError("'bytes_' and 'messages' have to be "
 
459
                            "integers or longs.")
 
460
        if force is not None and force != 'force':
 
461
            raise DomainError(_(u"Invalid argument: '%s'") % force,
 
462
                              INVALID_ARGUMENT)
 
463
        dom = self._get_domain(domainname)
 
464
        quotalimit = QuotaLimit(self._dbh, bytes=bytes_, messages=messages)
 
465
        if force is None:
 
466
            dom.update_quotalimit(quotalimit)
 
467
        else:
 
468
            dom.update_quotalimit(quotalimit, force=True)
 
469
 
 
470
    def domain_services(self, domainname, force=None, *services):
 
471
        """Wrapper around Domain.update_serviceset()."""
 
472
        kwargs = dict.fromkeys(SERVICES, False)
 
473
        if force is not None and force != 'force':
 
474
            raise DomainError(_(u"Invalid argument: '%s'") % force,
 
475
                              INVALID_ARGUMENT)
 
476
        for service in set(services):
 
477
            if service not in SERVICES:
 
478
                raise DomainError(_(u"Unknown service: '%s'") % service,
 
479
                                  UNKNOWN_SERVICE)
 
480
            kwargs[service] = True
 
481
 
 
482
        dom = self._get_domain(domainname)
 
483
        serviceset = ServiceSet(self._dbh, **kwargs)
 
484
        dom.update_serviceset(serviceset, (True, False)[not force])
 
485
 
 
486
    def domain_transport(self, domainname, transport, force=None):
 
487
        """Wrapper around Domain.update_transport()"""
 
488
        if force is not None and force != 'force':
 
489
            raise DomainError(_(u"Invalid argument: '%s'") % force,
 
490
                              INVALID_ARGUMENT)
 
491
        dom = self._get_domain(domainname)
 
492
        trsp = Transport(self._dbh, transport=transport)
 
493
        if force is None:
 
494
            dom.update_transport(trsp)
 
495
        else:
 
496
            dom.update_transport(trsp, force=True)
 
497
 
 
498
    def domain_note(self, domainname, note):
 
499
        """Wrapper around Domain.update_note()"""
 
500
        dom = self._get_domain(domainname)
 
501
        dom.update_note(note)
 
502
 
 
503
    def domain_delete(self, domainname, force=False):
 
504
        """Wrapper around Domain.delete()"""
 
505
        if not isinstance(force, bool):
 
506
            raise TypeError('force must be a bool')
 
507
        dom = self._get_domain(domainname)
 
508
        gid = dom.gid
 
509
        domdir = dom.directory
 
510
        if self._cfg.dget('domain.force_deletion') or force:
 
511
            dom.delete(True)
 
512
        else:
 
513
            dom.delete(False)
 
514
        if self._cfg.dget('domain.delete_directory'):
 
515
            self._delete_domain_dir(domdir, gid)
 
516
 
 
517
    def domain_info(self, domainname, details=None):
 
518
        """Wrapper around Domain.get_info(), Domain.get_accounts(),
 
519
        Domain.get_aliase_names(), Domain.get_aliases() and
 
520
        Domain.get_relocated."""
 
521
        if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full',
 
522
                           'relocated', 'catchall']:
 
523
            raise VMMError(_(u"Invalid argument: '%s'") % details,
 
524
                           INVALID_ARGUMENT)
 
525
        dom = self._get_domain(domainname)
 
526
        dominfo = dom.get_info()
 
527
        if dominfo['domain name'].startswith('xn--'):
 
528
            dominfo['domain name'] += ' (%s)' % \
 
529
                                      dominfo['domain name'].decode('idna')
 
530
        if details is None:
 
531
            return dominfo
 
532
        elif details == 'accounts':
 
533
            return (dominfo, dom.get_accounts())
 
534
        elif details == 'aliasdomains':
 
535
            return (dominfo, dom.get_aliase_names())
 
536
        elif details == 'aliases':
 
537
            return (dominfo, dom.get_aliases())
 
538
        elif details == 'relocated':
 
539
            return(dominfo, dom.get_relocated())
 
540
        elif details == 'catchall':
 
541
            return(dominfo, dom.get_catchall())
 
542
        else:
 
543
            return (dominfo, dom.get_aliase_names(), dom.get_accounts(),
 
544
                    dom.get_aliases(), dom.get_relocated(), dom.get_catchall())
 
545
 
 
546
    def aliasdomain_add(self, aliasname, domainname):
 
547
        """Adds an alias domain to the domain.
 
548
 
 
549
        Arguments:
 
550
 
 
551
        `aliasname` : basestring
 
552
          The name of the alias domain
 
553
        `domainname` : basestring
 
554
          The name of the target domain
 
555
        """
 
556
        dom = self._get_domain(domainname)
 
557
        alias_dom = AliasDomain(self._dbh, aliasname)
 
558
        alias_dom.set_destination(dom)
 
559
        alias_dom.save()
 
560
 
 
561
    def aliasdomain_info(self, aliasname):
 
562
        """Returns a dict (keys: "alias" and "domain") with the names of
 
563
        the alias domain and its primary domain."""
 
564
        self._db_connect()
 
565
        alias_dom = AliasDomain(self._dbh, aliasname)
 
566
        return alias_dom.info()
 
567
 
 
568
    def aliasdomain_switch(self, aliasname, domainname):
 
569
        """Modifies the target domain of an existing alias domain.
 
570
 
 
571
        Arguments:
 
572
 
 
573
        `aliasname` : basestring
 
574
          The name of the alias domain
 
575
        `domainname` : basestring
 
576
          The name of the new target domain
 
577
        """
 
578
        dom = self._get_domain(domainname)
 
579
        alias_dom = AliasDomain(self._dbh, aliasname)
 
580
        alias_dom.set_destination(dom)
 
581
        alias_dom.switch()
 
582
 
 
583
    def aliasdomain_delete(self, aliasname):
 
584
        """Deletes the given alias domain.
 
585
 
 
586
        Argument:
 
587
 
 
588
        `aliasname` : basestring
 
589
          The name of the alias domain
 
590
        """
 
591
        self._db_connect()
 
592
        alias_dom = AliasDomain(self._dbh, aliasname)
 
593
        alias_dom.delete()
 
594
 
 
595
    def domain_list(self, pattern=None):
 
596
        """Wrapper around function search() from module Domain."""
 
597
        from VirtualMailManager.domain import search
 
598
        like = False
 
599
        if pattern and (pattern.startswith('%') or pattern.endswith('%')):
 
600
            like = True
 
601
            if not re.match(RE_DOMAIN_SEARCH, pattern.strip('%')):
 
602
                raise VMMError(_(u"The pattern '%s' contains invalid "
 
603
                                 u"characters.") % pattern, DOMAIN_INVALID)
 
604
        self._db_connect()
 
605
        return search(self._dbh, pattern=pattern, like=like)
 
606
 
 
607
    def address_list(self, typelimit, pattern=None):
 
608
        """TODO"""
 
609
        llike = dlike = False
 
610
        lpattern = dpattern = None
 
611
        if pattern:
 
612
            parts = pattern.split('@', 2)
 
613
            if len(parts) == 2:
 
614
                # The pattern includes '@', so let's treat the
 
615
                # parts separately to allow for pattern search like %@domain.%
 
616
                lpattern = parts[0]
 
617
                llike = lpattern.startswith('%') or lpattern.endswith('%')
 
618
                dpattern = parts[1]
 
619
                dlike = dpattern.startswith('%') or dpattern.endswith('%')
 
620
 
 
621
                if llike:
 
622
                    checkp = lpattern.strip('%')
 
623
                else:
 
624
                    checkp = lpattern
 
625
                if len(checkp) > 0 and re.search(RE_LOCALPART, checkp):
 
626
                    raise VMMError(_(u"The pattern '%s' contains invalid "
 
627
                                     u"characters.") % pattern, LOCALPART_INVALID)
 
628
            else:
 
629
                # else just match on domains
 
630
                # (or should that be local part, I don't know…)
 
631
                dpattern = parts[0]
 
632
                dlike = dpattern.startswith('%') or dpattern.endswith('%')
 
633
 
 
634
            if dlike:
 
635
                checkp = dpattern.strip('%')
 
636
            else:
 
637
                checkp = dpattern
 
638
            if len(checkp) > 0 and not re.match(RE_DOMAIN_SEARCH, checkp):
 
639
                raise VMMError(_(u"The pattern '%s' contains invalid "
 
640
                                 u"characters.") % pattern, DOMAIN_INVALID)
 
641
        self._db_connect()
 
642
        from VirtualMailManager.common import search_addresses
 
643
        return search_addresses(self._dbh, typelimit=typelimit,
 
644
                                lpattern=lpattern, llike=llike,
 
645
                                dpattern=dpattern, dlike=dlike)
 
646
 
 
647
    def user_add(self, emailaddress, password):
 
648
        """Wrapper around Account.set_password() and Account.save()."""
 
649
        acc = self._get_account(emailaddress)
 
650
        if acc:
 
651
            raise VMMError(_(u"The account '%s' already exists.") %
 
652
                           acc.address, ACCOUNT_EXISTS)
 
653
        self._is_other_address(acc.address, TYPE_ACCOUNT)
 
654
        acc.set_password(password)
 
655
        acc.save()
 
656
        self._make_account_dirs(acc)
 
657
 
 
658
    def alias_add(self, aliasaddress, *targetaddresses):
 
659
        """Creates a new `Alias` entry for the given *aliasaddress* with
 
660
        the given *targetaddresses*."""
 
661
        alias = self._get_alias(aliasaddress)
 
662
        if not alias:
 
663
            self._is_other_address(alias.address, TYPE_ALIAS)
 
664
        destinations = [DestinationEmailAddress(addr, self._dbh) \
 
665
                for addr in targetaddresses]
 
666
        warnings = []
 
667
        destinations = alias.add_destinations(destinations, warnings)
 
668
        if warnings:
 
669
            self._warnings.append(_('Ignored destination addresses:'))
 
670
            self._warnings.extend(('  * %s' % w for w in warnings))
 
671
        for destination in destinations:
 
672
            if destination.gid and \
 
673
               not self._chk_other_address_types(destination, TYPE_RELOCATED):
 
674
                self._warnings.append(_(u"The destination account/alias '%s' "
 
675
                                        u"does not exist.") % destination)
 
676
 
 
677
    def user_delete(self, emailaddress, force=False):
 
678
        """Wrapper around Account.delete(...)"""
 
679
        if not isinstance(force, bool):
 
680
            raise TypeError('force must be a bool')
 
681
        acc = self._get_account(emailaddress)
 
682
        if not acc:
 
683
            raise VMMError(_(u"The account '%s' does not exist.") %
 
684
                           acc.address, NO_SUCH_ACCOUNT)
 
685
        uid = acc.uid
 
686
        gid = acc.gid
 
687
        dom_dir = acc.domain.directory
 
688
        acc_dir = acc.home
 
689
        acc.delete(force)
 
690
        if self._cfg.dget('account.delete_directory'):
 
691
            try:
 
692
                self._delete_home(dom_dir, uid, gid)
 
693
            except VMMError, err:
 
694
                if err.code in (FOUND_DOTS_IN_PATH, MAILDIR_PERM_MISMATCH,
 
695
                                NO_SUCH_DIRECTORY):
 
696
                    warning = _(u"""\
 
697
The account has been successfully deleted from the database.
 
698
    But an error occurred while deleting the following directory:
 
699
    '%(directory)s'
 
700
    Reason: %(reason)s""") % {'directory': acc_dir, 'reason': err.msg}
 
701
                    self._warnings.append(warning)
 
702
                else:
 
703
                    raise
 
704
 
 
705
    def alias_info(self, aliasaddress):
 
706
        """Returns an iterator object for all destinations (`EmailAddress`
 
707
        instances) for the `Alias` with the given *aliasaddress*."""
 
708
        alias = self._get_alias(aliasaddress)
 
709
        if alias:
 
710
            return alias.get_destinations()
 
711
        if not self._is_other_address(alias.address, TYPE_ALIAS):
 
712
            raise VMMError(_(u"The alias '%s' does not exist.") %
 
713
                           alias.address, NO_SUCH_ALIAS)
 
714
 
 
715
    def alias_delete(self, aliasaddress, targetaddress=None):
 
716
        """Deletes the `Alias` *aliasaddress* with all its destinations from
 
717
        the database. If *targetaddress* is not ``None``, only this
 
718
        destination will be removed from the alias."""
 
719
        alias = self._get_alias(aliasaddress)
 
720
        if targetaddress is None:
 
721
            alias.delete()
 
722
        else:
 
723
            alias.del_destination(DestinationEmailAddress(targetaddress,
 
724
                                                          self._dbh))
 
725
 
 
726
    def catchall_add(self, domain, *targetaddresses):
 
727
        """Creates a new `CatchallAlias` entry for the given *domain* with
 
728
        the given *targetaddresses*."""
 
729
        catchall = self._get_catchall(domain)
 
730
        destinations = [DestinationEmailAddress(addr, self._dbh) \
 
731
                for addr in targetaddresses]
 
732
        warnings = []
 
733
        destinations = catchall.add_destinations(destinations, warnings)
 
734
        if warnings:
 
735
            self._warnings.append(_('Ignored destination addresses:'))
 
736
            self._warnings.extend(('  * %s' % w for w in warnings))
 
737
        for destination in destinations:
 
738
            if destination.gid and \
 
739
               not self._chk_other_address_types(destination, TYPE_RELOCATED):
 
740
                self._warnings.append(_(u"The destination account/alias '%s' "
 
741
                                        u"does not exist.") % destination)
 
742
 
 
743
    def catchall_info(self, domain):
 
744
        """Returns an iterator object for all destinations (`EmailAddress`
 
745
        instances) for the `CatchallAlias` with the given *domain*."""
 
746
        return self._get_catchall(domain).get_destinations()
 
747
 
 
748
    def catchall_delete(self, domain, targetaddress=None):
 
749
        """Deletes the `CatchallAlias` for domain *domain* with all its
 
750
        destinations from the database. If *targetaddress* is not ``None``,
 
751
        only this destination will be removed from the alias."""
 
752
        catchall = self._get_catchall(domain)
 
753
        if targetaddress is None:
 
754
            catchall.delete()
 
755
        else:
 
756
            catchall.del_destination(DestinationEmailAddress(targetaddress,
 
757
                                                             self._dbh))
 
758
 
 
759
    def user_info(self, emailaddress, details=None):
 
760
        """Wrapper around Account.get_info(...)"""
 
761
        if details not in (None, 'du', 'aliases', 'full'):
 
762
            raise VMMError(_(u"Invalid argument: '%s'") % details,
 
763
                           INVALID_ARGUMENT)
 
764
        acc = self._get_account(emailaddress)
 
765
        if not acc:
 
766
            if not self._is_other_address(acc.address, TYPE_ACCOUNT):
 
767
                raise VMMError(_(u"The account '%s' does not exist.") %
 
768
                               acc.address, NO_SUCH_ACCOUNT)
 
769
        info = acc.get_info()
 
770
        if self._cfg.dget('account.disk_usage') or details in ('du', 'full'):
 
771
            path = os.path.join(acc.home, acc.mail_location.directory)
 
772
            info['disk usage'] = self._get_disk_usage(path)
 
773
            if details in (None, 'du'):
 
774
                return info
 
775
        if details in ('aliases', 'full'):
 
776
            return (info, acc.get_aliases())
 
777
        return info
 
778
 
 
779
    def user_by_uid(self, uid):
 
780
        """Search for an Account by its *uid*.
 
781
        Returns a dict (address, uid and gid) if a user could be found."""
 
782
        from VirtualMailManager.account import get_account_by_uid
 
783
        self._db_connect()
 
784
        return get_account_by_uid(uid, self._dbh)
 
785
 
 
786
    def user_password(self, emailaddress, password):
 
787
        """Wrapper for Account.modify('password' ...)."""
 
788
        if not isinstance(password, basestring) or not password:
 
789
            raise VMMError(_(u"Could not accept password: '%s'") % password,
 
790
                           INVALID_ARGUMENT)
 
791
        acc = self._get_account(emailaddress)
 
792
        if not acc:
 
793
            raise VMMError(_(u"The account '%s' does not exist.") %
 
794
                           acc.address, NO_SUCH_ACCOUNT)
 
795
        acc.modify('password', password)
 
796
 
 
797
    def user_name(self, emailaddress, name):
 
798
        """Wrapper for Account.modify('name', ...)."""
 
799
        acc = self._get_account(emailaddress)
 
800
        if not acc:
 
801
            raise VMMError(_(u"The account '%s' does not exist.") %
 
802
                           acc.address, NO_SUCH_ACCOUNT)
 
803
        acc.modify('name', name)
 
804
 
 
805
    def user_note(self, emailaddress, note):
 
806
        """Wrapper for Account.modify('note', ...)."""
 
807
        acc = self._get_account(emailaddress)
 
808
        if not acc:
 
809
            raise VMMError(_(u"The account '%s' does not exist.") %
 
810
                           acc.address, NO_SUCH_ACCOUNT)
 
811
        acc.modify('note', note)
 
812
 
 
813
    def user_quotalimit(self, emailaddress, bytes_, messages=0):
 
814
        """Wrapper for Account.update_quotalimit(QuotaLimit)."""
 
815
        acc = self._get_account(emailaddress)
 
816
        if not acc:
 
817
            raise VMMError(_(u"The account '%s' does not exist.") %
 
818
                        acc.address, NO_SUCH_ACCOUNT)
 
819
        if bytes_ == 'default':
 
820
            quotalimit = None
 
821
        else:
 
822
            if not all(isinstance(i, (int, long)) for i in (bytes_, messages)):
 
823
                raise TypeError("'bytes_' and 'messages' have to be "
 
824
                                "integers or longs.")
 
825
            quotalimit = QuotaLimit(self._dbh, bytes=bytes_,
 
826
                                    messages=messages)
 
827
        acc.update_quotalimit(quotalimit)
 
828
 
 
829
    def user_transport(self, emailaddress, transport):
 
830
        """Wrapper for Account.update_transport(Transport)."""
 
831
        if not isinstance(transport, basestring) or not transport:
 
832
            raise VMMError(_(u"Could not accept transport: '%s'") % transport,
 
833
                           INVALID_ARGUMENT)
 
834
        acc = self._get_account(emailaddress)
 
835
        if not acc:
 
836
            raise VMMError(_(u"The account '%s' does not exist.") %
 
837
                           acc.address, NO_SUCH_ACCOUNT)
 
838
        if transport == 'default':
 
839
            transport = None
 
840
        else:
 
841
            transport = Transport(self._dbh, transport=transport)
 
842
        acc.update_transport(transport)
 
843
 
 
844
    def user_services(self, emailaddress, *services):
 
845
        """Wrapper around Account.update_serviceset()."""
 
846
        acc = self._get_account(emailaddress)
 
847
        if not acc:
 
848
            raise VMMError(_(u"The account '%s' does not exist.") %
 
849
                        acc.address, NO_SUCH_ACCOUNT)
 
850
        if len(services) == 1 and services[0] == 'default':
 
851
            serviceset = None
 
852
        else:
 
853
            kwargs = dict.fromkeys(SERVICES, False)
 
854
            for service in set(services):
 
855
                if service not in SERVICES:
 
856
                    raise VMMError(_(u"Unknown service: '%s'") % service,
 
857
                                UNKNOWN_SERVICE)
 
858
                kwargs[service] = True
 
859
            serviceset = ServiceSet(self._dbh, **kwargs)
 
860
        acc.update_serviceset(serviceset)
 
861
 
 
862
    def relocated_add(self, emailaddress, targetaddress):
 
863
        """Creates a new `Relocated` entry in the database. If there is
 
864
        already a relocated user with the given *emailaddress*, only the
 
865
        *targetaddress* for the relocated user will be updated."""
 
866
        relocated = self._get_relocated(emailaddress)
 
867
        if not relocated:
 
868
            self._is_other_address(relocated.address, TYPE_RELOCATED)
 
869
        destination = DestinationEmailAddress(targetaddress, self._dbh)
 
870
        relocated.set_destination(destination)
 
871
        if destination.gid and \
 
872
           not self._chk_other_address_types(destination, TYPE_RELOCATED):
 
873
            self._warnings.append(_(u"The destination account/alias '%s' "
 
874
                                    u"does not exist.") % destination)
 
875
 
 
876
    def relocated_info(self, emailaddress):
 
877
        """Returns the target address of the relocated user with the given
 
878
        *emailaddress*."""
 
879
        relocated = self._get_relocated(emailaddress)
 
880
        if relocated:
 
881
            return relocated.get_info()
 
882
        if not self._is_other_address(relocated.address, TYPE_RELOCATED):
 
883
            raise VMMError(_(u"The relocated user '%s' does not exist.") %
 
884
                           relocated.address, NO_SUCH_RELOCATED)
 
885
 
 
886
    def relocated_delete(self, emailaddress):
 
887
        """Deletes the relocated user with the given *emailaddress* from
 
888
        the database."""
 
889
        relocated = self._get_relocated(emailaddress)
 
890
        relocated.delete()
 
891
 
 
892
del _