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