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