1
# -*- coding: UTF-8 -*-
2
# Copyright (c) 2007 - 2012, Pascal Volk
3
# See COPYING for distribution information.
5
VirtualMailManager.handler
6
~~~~~~~~~~~~~~~~~~~~~~~~~~
8
A wrapper class. It wraps round all other classes and does some
11
Additionally it communicates with the PostgreSQL database, creates
12
or deletes directories of domains or users.
18
from shutil import rmtree
19
from subprocess import Popen, PIPE
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
51
CFG_DB_FILE = 'vmm-db.cfg'
53
RE_DOMAIN_SEARCH = """^[a-z0-9-\.]+$"""
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),
60
class Handler(object):
61
"""Wrapper class to simplify the access on all the stuff from
63
__slots__ = ('_cfg', '_cfg_fname', '_db_connect', '_dbh', '_warnings')
65
def __init__(self, skip_some_checks=False):
66
"""Creates a new Handler instance.
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.
73
Throws a NotRootError if your uid is greater 0.
79
self._db_connect = None
82
raise NotRootError(_(u"You are not root.\n\tGood bye!\n"),
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)
89
if not skip_some_checks:
92
self._set_db_connect()
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.
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
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)
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)
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"""
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)
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},
140
for opt, val in self._cfg.items('bin'):
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)
152
def _set_db_connect(self):
153
"""check which module to use and set self._db_connect"""
155
if self._cfg.dget('database.module').lower() == 'psycopg2':
157
_db_mod = __import__('psycopg2')
159
raise VMMError(_(u"Unable to import database module '%s'.") %
160
'psycopg2', VMM_ERROR)
161
self._db_connect = self._psycopg2_connect
164
tmp = __import__('pyPgSQL', globals(), locals(), ['PgSQL'])
166
raise VMMError(_(u"Unable to import database module '%s'.") %
167
'pyPgSQL', VMM_ERROR)
169
self._db_connect = self._pypgsql_connect
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):
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'")
186
except _db_mod.libpq.DatabaseError, err:
187
raise VMMError(str(err), DATABASE_ERROR)
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
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'")
207
except _db_mod.DatabaseError, err:
208
raise VMMError(str(err), DATABASE_ERROR)
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
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)
222
if exclude is not TYPE_ALIAS:
223
alias = Alias(self._dbh, address)
226
if exclude is not TYPE_RELOCATED:
227
relocated = Relocated(self._dbh, address)
229
return TYPE_RELOCATED
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
238
Raises a `VMMError` if the address is known.
240
other = self._chk_other_address_types(address, exclude)
243
# TP: %(a_type)s will be one of: 'an account', 'an alias' or
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])
249
def _get_account(self, address):
250
"""Return an Account instances for the given address (str)."""
251
address = EmailAddress(address)
253
return Account(self._dbh, address)
255
def _get_alias(self, address):
256
"""Return an Alias instances for the given address (str)."""
257
address = EmailAddress(address)
259
return Alias(self._dbh, address)
261
def _get_catchall(self, domain):
262
"""Return a CatchallAlias instances for the given domain (str)."""
264
return CatchallAlias(self._dbh, domain)
266
def _get_relocated(self, address):
267
"""Return a Relocated instances for the given address (str)."""
268
address = EmailAddress(address)
270
return Relocated(self._dbh, address)
272
def _get_domain(self, domainname):
273
"""Return a Domain instances for the given domain name (str)."""
275
return Domain(self._dbh, domainname)
277
def _get_disk_usage(self, directory):
278
"""Estimate file space usage for the given directory.
282
`directory` : basestring
283
The directory to summarize recursively disk usage for
285
if lisdir(directory):
286
return Popen([self._cfg.dget('bin.du'), "-hs", directory],
287
stdout=PIPE).communicate()[0].split('\t')[0]
289
self._warnings.append(_('No such directory: %s') % directory)
292
def _make_domain_dir(self, domain):
293
"""Create a directory for the `domain` and its accounts."""
295
hashdir, domdir = domain.directory.split(os.path.sep)[-2:]
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)
303
if not dir_created and not lisdir(hashdir):
304
raise VMMError(_(u"'%s' is not a directory.") % hashdir,
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)
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)
323
os.mkdir('%s' % uid, self._cfg.dget('account.directory_mode'))
324
os.chown('%s' % uid, uid, account.gid)
326
def _make_account_dirs(self, account):
327
"""Create all necessary directories for the account."""
329
self._make_home(account)
330
mailbox = new_mailbox(account)
332
folders = self._cfg.dget('mailbox.folders').split(':')
334
bad = mailbox.add_boxes(folders,
335
self._cfg.dget('mailbox.subscribe'))
337
self._warnings.append(_(u"Skipped mailbox folders:") +
338
'\n\t- ' + '\n\t- '.join(bad))
341
def _delete_home(self, domdir, uid, gid):
342
"""Delete a user's home directory.
346
`domdir` : basestring
347
The directory of the domain the user belongs to
348
(commonly AccountObj.domain.directory)
350
The user's UID (commonly AccountObj.uid)
352
The user's GID (commonly AccountObj.gid)
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,
369
if not lisdir(userdir):
370
self._warnings.append(_(u"No such directory: %s") %
371
os.path.join(domdir, userdir))
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)
379
def _delete_domain_dir(self, domdir, gid):
380
"""Delete a domain's directory.
384
`domdir` : basestring
385
The domain's directory (commonly DomainObj.directory)
387
The domain's GID (commonly DomainObj.gid)
389
assert isinstance(domdir, basestring) and isinstance(gid, (long, int))
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)
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)
406
def has_warnings(self):
407
"""Checks if warnings are present, returns bool."""
408
return bool(len(self._warnings))
410
def get_warnings(self):
411
"""Returns a list with all available warnings and resets all
414
ret_val = self._warnings[:]
415
del self._warnings[:]
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
422
return self._cfg.dget(option)
424
def cfg_pget(self, option):
425
"""Get the configured value of the *option* (section.option)."""
426
return self._cfg.pget(option)
428
def cfg_install(self):
429
"""Installs the cfg_dget method as ``cfg_dget`` into the built-in
432
assert 'cfg_dget' not in __builtin__.__dict__
433
__builtin__.__dict__['cfg_dget'] = self._cfg.dget
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')))
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'))
453
self._make_domain_dir(dom)
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,
463
dom = self._get_domain(domainname)
464
quotalimit = QuotaLimit(self._dbh, bytes=bytes_, messages=messages)
466
dom.update_quotalimit(quotalimit)
468
dom.update_quotalimit(quotalimit, force=True)
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,
476
for service in set(services):
477
if service not in SERVICES:
478
raise DomainError(_(u"Unknown service: '%s'") % service,
480
kwargs[service] = True
482
dom = self._get_domain(domainname)
483
serviceset = ServiceSet(self._dbh, **kwargs)
484
dom.update_serviceset(serviceset, (True, False)[not force])
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,
491
dom = self._get_domain(domainname)
492
trsp = Transport(self._dbh, transport=transport)
494
dom.update_transport(trsp)
496
dom.update_transport(trsp, force=True)
498
def domain_note(self, domainname, note):
499
"""Wrapper around Domain.update_note()"""
500
dom = self._get_domain(domainname)
501
dom.update_note(note)
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)
509
domdir = dom.directory
510
if self._cfg.dget('domain.force_deletion') or force:
514
if self._cfg.dget('domain.delete_directory'):
515
self._delete_domain_dir(domdir, gid)
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,
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')
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())
543
return (dominfo, dom.get_aliase_names(), dom.get_accounts(),
544
dom.get_aliases(), dom.get_relocated(), dom.get_catchall())
546
def aliasdomain_add(self, aliasname, domainname):
547
"""Adds an alias domain to the domain.
551
`aliasname` : basestring
552
The name of the alias domain
553
`domainname` : basestring
554
The name of the target domain
556
dom = self._get_domain(domainname)
557
alias_dom = AliasDomain(self._dbh, aliasname)
558
alias_dom.set_destination(dom)
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."""
565
alias_dom = AliasDomain(self._dbh, aliasname)
566
return alias_dom.info()
568
def aliasdomain_switch(self, aliasname, domainname):
569
"""Modifies the target domain of an existing alias domain.
573
`aliasname` : basestring
574
The name of the alias domain
575
`domainname` : basestring
576
The name of the new target domain
578
dom = self._get_domain(domainname)
579
alias_dom = AliasDomain(self._dbh, aliasname)
580
alias_dom.set_destination(dom)
583
def aliasdomain_delete(self, aliasname):
584
"""Deletes the given alias domain.
588
`aliasname` : basestring
589
The name of the alias domain
592
alias_dom = AliasDomain(self._dbh, aliasname)
595
def domain_list(self, pattern=None):
596
"""Wrapper around function search() from module Domain."""
597
from VirtualMailManager.domain import search
599
if pattern and (pattern.startswith('%') or pattern.endswith('%')):
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)
605
return search(self._dbh, pattern=pattern, like=like)
607
def address_list(self, typelimit, pattern=None):
609
llike = dlike = False
610
lpattern = dpattern = None
612
parts = pattern.split('@', 2)
614
# The pattern includes '@', so let's treat the
615
# parts separately to allow for pattern search like %@domain.%
617
llike = lpattern.startswith('%') or lpattern.endswith('%')
619
dlike = dpattern.startswith('%') or dpattern.endswith('%')
622
checkp = lpattern.strip('%')
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)
629
# else just match on domains
630
# (or should that be local part, I don't know…)
632
dlike = dpattern.startswith('%') or dpattern.endswith('%')
635
checkp = dpattern.strip('%')
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)
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)
647
def user_add(self, emailaddress, password):
648
"""Wrapper around Account.set_password() and Account.save()."""
649
acc = self._get_account(emailaddress)
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)
656
self._make_account_dirs(acc)
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)
663
self._is_other_address(alias.address, TYPE_ALIAS)
664
destinations = [DestinationEmailAddress(addr, self._dbh) \
665
for addr in targetaddresses]
667
destinations = alias.add_destinations(destinations, 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)
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)
683
raise VMMError(_(u"The account '%s' does not exist.") %
684
acc.address, NO_SUCH_ACCOUNT)
687
dom_dir = acc.domain.directory
690
if self._cfg.dget('account.delete_directory'):
692
self._delete_home(dom_dir, uid, gid)
693
except VMMError, err:
694
if err.code in (FOUND_DOTS_IN_PATH, MAILDIR_PERM_MISMATCH,
697
The account has been successfully deleted from the database.
698
But an error occurred while deleting the following directory:
700
Reason: %(reason)s""") % {'directory': acc_dir, 'reason': err.msg}
701
self._warnings.append(warning)
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)
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)
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:
723
alias.del_destination(DestinationEmailAddress(targetaddress,
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]
733
destinations = catchall.add_destinations(destinations, 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)
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()
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:
756
catchall.del_destination(DestinationEmailAddress(targetaddress,
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,
764
acc = self._get_account(emailaddress)
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'):
775
if details in ('aliases', 'full'):
776
return (info, acc.get_aliases())
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
784
return get_account_by_uid(uid, self._dbh)
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,
791
acc = self._get_account(emailaddress)
793
raise VMMError(_(u"The account '%s' does not exist.") %
794
acc.address, NO_SUCH_ACCOUNT)
795
acc.modify('password', password)
797
def user_name(self, emailaddress, name):
798
"""Wrapper for Account.modify('name', ...)."""
799
acc = self._get_account(emailaddress)
801
raise VMMError(_(u"The account '%s' does not exist.") %
802
acc.address, NO_SUCH_ACCOUNT)
803
acc.modify('name', name)
805
def user_note(self, emailaddress, note):
806
"""Wrapper for Account.modify('note', ...)."""
807
acc = self._get_account(emailaddress)
809
raise VMMError(_(u"The account '%s' does not exist.") %
810
acc.address, NO_SUCH_ACCOUNT)
811
acc.modify('note', note)
813
def user_quotalimit(self, emailaddress, bytes_, messages=0):
814
"""Wrapper for Account.update_quotalimit(QuotaLimit)."""
815
acc = self._get_account(emailaddress)
817
raise VMMError(_(u"The account '%s' does not exist.") %
818
acc.address, NO_SUCH_ACCOUNT)
819
if bytes_ == 'default':
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_,
827
acc.update_quotalimit(quotalimit)
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,
834
acc = self._get_account(emailaddress)
836
raise VMMError(_(u"The account '%s' does not exist.") %
837
acc.address, NO_SUCH_ACCOUNT)
838
if transport == 'default':
841
transport = Transport(self._dbh, transport=transport)
842
acc.update_transport(transport)
844
def user_services(self, emailaddress, *services):
845
"""Wrapper around Account.update_serviceset()."""
846
acc = self._get_account(emailaddress)
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':
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,
858
kwargs[service] = True
859
serviceset = ServiceSet(self._dbh, **kwargs)
860
acc.update_serviceset(serviceset)
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)
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)
876
def relocated_info(self, emailaddress):
877
"""Returns the target address of the relocated user with the given
879
relocated = self._get_relocated(emailaddress)
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)
886
def relocated_delete(self, emailaddress):
887
"""Deletes the relocated user with the given *emailaddress* from
889
relocated = self._get_relocated(emailaddress)