|
1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2007 - 2010, Pascal Volk |
|
3 # See COPYING for distribution information. |
|
4 |
|
5 """ |
|
6 VirtualMailManager.Handler |
|
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 pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net |
|
22 |
|
23 import VirtualMailManager.constants.ERROR as ERR |
|
24 from VirtualMailManager import ENCODING, ace2idna, exec_ok, read_pass |
|
25 from VirtualMailManager.Account import Account |
|
26 from VirtualMailManager.Alias import Alias |
|
27 from VirtualMailManager.AliasDomain import AliasDomain |
|
28 from VirtualMailManager.Config import Config as Cfg |
|
29 from VirtualMailManager.Domain import Domain |
|
30 from VirtualMailManager.EmailAddress import EmailAddress |
|
31 from VirtualMailManager.Exceptions import * |
|
32 from VirtualMailManager.Relocated import Relocated |
|
33 from VirtualMailManager.ext.Postconf import Postconf |
|
34 |
|
35 SALTCHARS = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' |
|
36 RE_DOMAIN_SRCH = """^[a-z0-9-\.]+$""" |
|
37 RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]""" |
|
38 RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$""" |
|
39 |
|
40 class Handler(object): |
|
41 """Wrapper class to simplify the access on all the stuff from |
|
42 VirtualMailManager""" |
|
43 # TODO: accept a LazyConfig object as argument |
|
44 __slots__ = ('__Cfg', '__cfgFileName', '__dbh', '__scheme', '__warnings', |
|
45 '_postconf') |
|
46 def __init__(self): |
|
47 """Creates a new Handler instance. |
|
48 Throws a VMMNotRootException if your uid is greater 0. |
|
49 """ |
|
50 self.__cfgFileName = '' |
|
51 self.__warnings = [] |
|
52 self.__Cfg = None |
|
53 self.__dbh = None |
|
54 |
|
55 if os.geteuid(): |
|
56 raise VMMNotRootException(_(u"You are not root.\n\tGood bye!\n"), |
|
57 ERR.CONF_NOPERM) |
|
58 if self.__chkCfgFile(): |
|
59 self.__Cfg = Cfg(self.__cfgFileName) |
|
60 self.__Cfg.load() |
|
61 if not os.sys.argv[1] in ('cf','configure','h','help','v','version'): |
|
62 self.__Cfg.check() |
|
63 self.__chkenv() |
|
64 self.__scheme = self.__Cfg.dget('misc.password_scheme') |
|
65 self._postconf = Postconf(self.__Cfg.dget('bin.postconf')) |
|
66 |
|
67 def __findCfgFile(self): |
|
68 for path in ['/root', '/usr/local/etc', '/etc']: |
|
69 tmp = os.path.join(path, 'vmm.cfg') |
|
70 if os.path.isfile(tmp): |
|
71 self.__cfgFileName = tmp |
|
72 break |
|
73 if not len(self.__cfgFileName): |
|
74 raise VMMException( |
|
75 _(u"No “vmm.cfg” found in: /root:/usr/local/etc:/etc"), |
|
76 ERR.CONF_NOFILE) |
|
77 |
|
78 def __chkCfgFile(self): |
|
79 """Checks the configuration file, returns bool""" |
|
80 self.__findCfgFile() |
|
81 fstat = os.stat(self.__cfgFileName) |
|
82 fmode = int(oct(fstat.st_mode & 0777)) |
|
83 if fmode % 100 and fstat.st_uid != fstat.st_gid \ |
|
84 or fmode % 10 and fstat.st_uid == fstat.st_gid: |
|
85 raise VMMPermException(_( |
|
86 u'fix permissions (%(perms)s) for “%(file)s”\n\ |
|
87 `chmod 0600 %(file)s` would be great.') % {'file': |
|
88 self.__cfgFileName, 'perms': fmode}, ERR.CONF_WRONGPERM) |
|
89 else: |
|
90 return True |
|
91 |
|
92 def __chkenv(self): |
|
93 """""" |
|
94 basedir = self.__Cfg.dget('misc.base_directory') |
|
95 if not os.path.exists(basedir): |
|
96 old_umask = os.umask(0006) |
|
97 os.makedirs(basedir, 0771) |
|
98 os.chown(basedir, 0, self.__Cfg.dget('misc.gid_mail')) |
|
99 os.umask(old_umask) |
|
100 elif not os.path.isdir(basedir): |
|
101 raise VMMException(_(u'“%s” is not a directory.\n\ |
|
102 (vmm.cfg: section "misc", option "base_directory")') % |
|
103 basedir, ERR.NO_SUCH_DIRECTORY) |
|
104 for opt, val in self.__Cfg.items('bin'): |
|
105 try: |
|
106 exec_ok(val) |
|
107 except VMMException, e: |
|
108 code = e.code() |
|
109 if code is ERR.NO_SUCH_BINARY: |
|
110 raise VMMException(_(u'“%(binary)s” doesn\'t exist.\n\ |
|
111 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt}, |
|
112 ERR.NO_SUCH_BINARY) |
|
113 elif code is ERR.NOT_EXECUTABLE: |
|
114 raise VMMException(_(u'“%(binary)s” is not executable.\n\ |
|
115 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt}, |
|
116 ERR.NOT_EXECUTABLE) |
|
117 else: |
|
118 raise |
|
119 |
|
120 def __dbConnect(self): |
|
121 """Creates a pyPgSQL.PgSQL.connection instance.""" |
|
122 if self.__dbh is None or (isinstance(self.__dbh, PgSQL.Connection) and |
|
123 not self.__dbh._isOpen): |
|
124 try: |
|
125 self.__dbh = PgSQL.connect( |
|
126 database=self.__Cfg.dget('database.name'), |
|
127 user=self.__Cfg.pget('database.user'), |
|
128 host=self.__Cfg.dget('database.host'), |
|
129 password=self.__Cfg.pget('database.pass'), |
|
130 client_encoding='utf8', unicode_results=True) |
|
131 dbc = self.__dbh.cursor() |
|
132 dbc.execute("SET NAMES 'UTF8'") |
|
133 dbc.close() |
|
134 except PgSQL.libpq.DatabaseError, e: |
|
135 raise VMMException(str(e), ERR.DATABASE_ERROR) |
|
136 |
|
137 def _exists(dbh, query): |
|
138 dbc = dbh.cursor() |
|
139 dbc.execute(query) |
|
140 gid = dbc.fetchone() |
|
141 dbc.close() |
|
142 if gid is None: |
|
143 return False |
|
144 else: |
|
145 return True |
|
146 _exists = staticmethod(_exists) |
|
147 |
|
148 def accountExists(dbh, address): |
|
149 sql = "SELECT gid FROM users WHERE gid = (SELECT gid FROM domain_name\ |
|
150 WHERE domainname = '%s') AND local_part = '%s'" % (address._domainname, |
|
151 address._localpart) |
|
152 return Handler._exists(dbh, sql) |
|
153 accountExists = staticmethod(accountExists) |
|
154 |
|
155 def aliasExists(dbh, address): |
|
156 sql = "SELECT DISTINCT gid FROM alias WHERE gid = (SELECT gid FROM\ |
|
157 domain_name WHERE domainname = '%s') AND address = '%s'" %\ |
|
158 (address._domainname, address._localpart) |
|
159 return Handler._exists(dbh, sql) |
|
160 aliasExists = staticmethod(aliasExists) |
|
161 |
|
162 def relocatedExists(dbh, address): |
|
163 sql = "SELECT gid FROM relocated WHERE gid = (SELECT gid FROM\ |
|
164 domain_name WHERE domainname = '%s') AND address = '%s'" %\ |
|
165 (address._domainname, address._localpart) |
|
166 return Handler._exists(dbh, sql) |
|
167 relocatedExists = staticmethod(relocatedExists) |
|
168 |
|
169 |
|
170 def __getAccount(self, address, password=None): |
|
171 self.__dbConnect() |
|
172 address = EmailAddress(address) |
|
173 if not password is None: |
|
174 password = self.__pwhash(password) |
|
175 return Account(self.__dbh, address, password) |
|
176 |
|
177 def __getAlias(self, address, destination=None): |
|
178 self.__dbConnect() |
|
179 address = EmailAddress(address) |
|
180 if destination is not None: |
|
181 destination = EmailAddress(destination) |
|
182 return Alias(self.__dbh, address, destination) |
|
183 |
|
184 def __getRelocated(self,address, destination=None): |
|
185 self.__dbConnect() |
|
186 address = EmailAddress(address) |
|
187 if destination is not None: |
|
188 destination = EmailAddress(destination) |
|
189 return Relocated(self.__dbh, address, destination) |
|
190 |
|
191 def __getDomain(self, domainname, transport=None): |
|
192 if transport is None: |
|
193 transport = self.__Cfg.dget('misc.transport') |
|
194 self.__dbConnect() |
|
195 return Domain(self.__dbh, domainname, |
|
196 self.__Cfg.dget('misc.base_directory'), transport) |
|
197 |
|
198 def __getDiskUsage(self, directory): |
|
199 """Estimate file space usage for the given directory. |
|
200 |
|
201 Keyword arguments: |
|
202 directory -- the directory to summarize recursively disk usage for |
|
203 """ |
|
204 if self.__isdir(directory): |
|
205 return Popen([self.__Cfg.dget('bin.du'), "-hs", directory], |
|
206 stdout=PIPE).communicate()[0].split('\t')[0] |
|
207 else: |
|
208 return 0 |
|
209 |
|
210 def __isdir(self, directory): |
|
211 isdir = os.path.isdir(directory) |
|
212 if not isdir: |
|
213 self.__warnings.append(_('No such directory: %s') % directory) |
|
214 return isdir |
|
215 |
|
216 def __makedir(self, directory, mode=None, uid=None, gid=None): |
|
217 if mode is None: |
|
218 mode = self.__Cfg.dget('account.directory_mode') |
|
219 if uid is None: |
|
220 uid = 0 |
|
221 if gid is None: |
|
222 gid = 0 |
|
223 os.makedirs(directory, mode) |
|
224 os.chown(directory, uid, gid) |
|
225 |
|
226 def __domDirMake(self, domdir, gid): |
|
227 os.umask(0006) |
|
228 oldpwd = os.getcwd() |
|
229 basedir = self.__Cfg.dget('misc.base_directory') |
|
230 domdirdirs = domdir.replace(basedir+'/', '').split('/') |
|
231 |
|
232 os.chdir(basedir) |
|
233 if not os.path.isdir(domdirdirs[0]): |
|
234 self.__makedir(domdirdirs[0], 489, 0, |
|
235 self.__Cfg.dget('misc.gid_mail')) |
|
236 os.chdir(domdirdirs[0]) |
|
237 os.umask(0007) |
|
238 self.__makedir(domdirdirs[1], self.__Cfg.dget('domain.directory_mode'), |
|
239 0, gid) |
|
240 os.chdir(oldpwd) |
|
241 |
|
242 def __subscribeFL(self, folderlist, uid, gid): |
|
243 fname = os.path.join(self.__Cfg.dget('maildir.name'), 'subscriptions') |
|
244 sf = file(fname, 'w') |
|
245 for f in folderlist: |
|
246 sf.write(f+'\n') |
|
247 sf.flush() |
|
248 sf.close() |
|
249 os.chown(fname, uid, gid) |
|
250 os.chmod(fname, 384) |
|
251 |
|
252 def __mailDirMake(self, domdir, uid, gid): |
|
253 """Creates maildirs and maildir subfolders. |
|
254 |
|
255 Keyword arguments: |
|
256 domdir -- the path to the domain directory |
|
257 uid -- user id from the account |
|
258 gid -- group id from the account |
|
259 """ |
|
260 os.umask(0007) |
|
261 oldpwd = os.getcwd() |
|
262 os.chdir(domdir) |
|
263 |
|
264 maildir = self.__Cfg.dget('maildir.name') |
|
265 folders = [maildir] |
|
266 for folder in self.__Cfg.dget('maildir.folders').split(':'): |
|
267 folder = folder.strip() |
|
268 if len(folder) and not folder.count('..')\ |
|
269 and re.match(RE_MBOX_NAMES, folder): |
|
270 folders.append('%s/.%s' % (maildir, folder)) |
|
271 subdirs = ['cur', 'new', 'tmp'] |
|
272 mode = self.__Cfg.dget('account.directory_mode') |
|
273 |
|
274 self.__makedir('%s' % uid, mode, uid, gid) |
|
275 os.chdir('%s' % uid) |
|
276 for folder in folders: |
|
277 self.__makedir(folder, mode, uid, gid) |
|
278 for subdir in subdirs: |
|
279 self.__makedir(os.path.join(folder, subdir), mode, uid, gid) |
|
280 self.__subscribeFL([f.replace(maildir+'/.', '') for f in folders[1:]], |
|
281 uid, gid) |
|
282 os.chdir(oldpwd) |
|
283 |
|
284 def __userDirDelete(self, domdir, uid, gid): |
|
285 if uid > 0 and gid > 0: |
|
286 userdir = '%s' % uid |
|
287 if userdir.count('..') or domdir.count('..'): |
|
288 raise VMMException(_(u'Found ".." in home directory path.'), |
|
289 ERR.FOUND_DOTS_IN_PATH) |
|
290 if os.path.isdir(domdir): |
|
291 os.chdir(domdir) |
|
292 if os.path.isdir(userdir): |
|
293 mdstat = os.stat(userdir) |
|
294 if (mdstat.st_uid, mdstat.st_gid) != (uid, gid): |
|
295 raise VMMException( |
|
296 _(u'Detected owner/group mismatch in home directory.'), |
|
297 ERR.MAILDIR_PERM_MISMATCH) |
|
298 rmtree(userdir, ignore_errors=True) |
|
299 else: |
|
300 raise VMMException(_(u"No such directory: %s") % |
|
301 os.path.join(domdir, userdir), ERR.NO_SUCH_DIRECTORY) |
|
302 |
|
303 def __domDirDelete(self, domdir, gid): |
|
304 if gid > 0: |
|
305 if not self.__isdir(domdir): |
|
306 return |
|
307 basedir = self.__Cfg.dget('misc.base_directory') |
|
308 domdirdirs = domdir.replace(basedir+'/', '').split('/') |
|
309 domdirparent = os.path.join(basedir, domdirdirs[0]) |
|
310 if basedir.count('..') or domdir.count('..'): |
|
311 raise VMMException(_(u'Found ".." in domain directory path.'), |
|
312 ERR.FOUND_DOTS_IN_PATH) |
|
313 if os.path.isdir(domdirparent): |
|
314 os.chdir(domdirparent) |
|
315 if os.lstat(domdirdirs[1]).st_gid != gid: |
|
316 raise VMMException(_( |
|
317 u'Detected group mismatch in domain directory.'), |
|
318 ERR.DOMAINDIR_GROUP_MISMATCH) |
|
319 rmtree(domdirdirs[1], ignore_errors=True) |
|
320 |
|
321 def __getSalt(self): |
|
322 from random import choice |
|
323 salt = None |
|
324 if self.__scheme == 'CRYPT': |
|
325 salt = '%s%s' % (choice(SALTCHARS), choice(SALTCHARS)) |
|
326 elif self.__scheme in ['MD5', 'MD5-CRYPT']: |
|
327 salt = '$1$%s$' % ''.join([choice(SALTCHARS) for x in xrange(8)]) |
|
328 return salt |
|
329 |
|
330 def __pwCrypt(self, password): |
|
331 # for: CRYPT, MD5 and MD5-CRYPT |
|
332 from crypt import crypt |
|
333 return crypt(password, self.__getSalt()) |
|
334 |
|
335 def __pwSHA1(self, password): |
|
336 # for: SHA/SHA1 |
|
337 import sha |
|
338 from base64 import standard_b64encode |
|
339 sha1 = sha.new(password) |
|
340 return standard_b64encode(sha1.digest()) |
|
341 |
|
342 def __pwMD5(self, password, emailaddress=None): |
|
343 import md5 |
|
344 _md5 = md5.new(password) |
|
345 if self.__scheme == 'LDAP-MD5': |
|
346 from base64 import standard_b64encode |
|
347 return standard_b64encode(_md5.digest()) |
|
348 elif self.__scheme == 'PLAIN-MD5': |
|
349 return _md5.hexdigest() |
|
350 elif self.__scheme == 'DIGEST-MD5' and emailaddress is not None: |
|
351 # use an empty realm - works better with usenames like user@dom |
|
352 _md5 = md5.new('%s::%s' % (emailaddress, password)) |
|
353 return _md5.hexdigest() |
|
354 |
|
355 def __pwMD4(self, password): |
|
356 # for: PLAIN-MD4 |
|
357 from Crypto.Hash import MD4 |
|
358 _md4 = MD4.new(password) |
|
359 return _md4.hexdigest() |
|
360 |
|
361 def __pwhash(self, password, scheme=None, user=None): |
|
362 if scheme is not None: |
|
363 self.__scheme = scheme |
|
364 if self.__scheme in ['CRYPT', 'MD5', 'MD5-CRYPT']: |
|
365 return '{%s}%s' % (self.__scheme, self.__pwCrypt(password)) |
|
366 elif self.__scheme in ['SHA', 'SHA1']: |
|
367 return '{%s}%s' % (self.__scheme, self.__pwSHA1(password)) |
|
368 elif self.__scheme in ['PLAIN-MD5', 'LDAP-MD5', 'DIGEST-MD5']: |
|
369 return '{%s}%s' % (self.__scheme, self.__pwMD5(password, user)) |
|
370 elif self.__scheme == 'MD4': |
|
371 return '{%s}%s' % (self.__scheme, self.__pwMD4(password)) |
|
372 elif self.__scheme in ['SMD5', 'SSHA', 'CRAM-MD5', 'HMAC-MD5', |
|
373 'LANMAN', 'NTLM', 'RPA']: |
|
374 return Popen([self.__Cfg.dget('bin.dovecotpw'), '-s', |
|
375 self.__scheme,'-p',password],stdout=PIPE).communicate()[0][:-1] |
|
376 else: |
|
377 return '{%s}%s' % (self.__scheme, password) |
|
378 |
|
379 def hasWarnings(self): |
|
380 """Checks if warnings are present, returns bool.""" |
|
381 return bool(len(self.__warnings)) |
|
382 |
|
383 def getWarnings(self): |
|
384 """Returns a list with all available warnings.""" |
|
385 return self.__warnings |
|
386 |
|
387 def cfgDget(self, option): |
|
388 return self.__Cfg.dget(option) |
|
389 |
|
390 def cfgPget(self, option): |
|
391 return self.__Cfg.pget(option) |
|
392 |
|
393 def cfgSet(self, option, value): |
|
394 return self.__Cfg.set(option, value) |
|
395 |
|
396 def configure(self, section=None): |
|
397 """Starts interactive configuration. |
|
398 |
|
399 Configures in interactive mode options in the given section. |
|
400 If no section is given (default) all options from all sections |
|
401 will be prompted. |
|
402 |
|
403 Keyword arguments: |
|
404 section -- the section to configure (default None): |
|
405 """ |
|
406 if section is None: |
|
407 self.__Cfg.configure(self.__Cfg.getsections()) |
|
408 elif self.__Cfg.has_section(section): |
|
409 self.__Cfg.configure([section]) |
|
410 else: |
|
411 raise VMMException(_(u"Invalid section: “%s”") % section, |
|
412 ERR.INVALID_SECTION) |
|
413 |
|
414 def domainAdd(self, domainname, transport=None): |
|
415 dom = self.__getDomain(domainname, transport) |
|
416 dom.save() |
|
417 self.__domDirMake(dom.getDir(), dom.getID()) |
|
418 |
|
419 def domainTransport(self, domainname, transport, force=None): |
|
420 if force is not None and force != 'force': |
|
421 raise VMMDomainException(_(u"Invalid argument: “%s”") % force, |
|
422 ERR.INVALID_OPTION) |
|
423 dom = self.__getDomain(domainname, None) |
|
424 if force is None: |
|
425 dom.updateTransport(transport) |
|
426 else: |
|
427 dom.updateTransport(transport, force=True) |
|
428 |
|
429 def domainDelete(self, domainname, force=None): |
|
430 if not force is None and force not in ['deluser','delalias','delall']: |
|
431 raise VMMDomainException(_(u"Invalid argument: “%s”") % force, |
|
432 ERR.INVALID_OPTION) |
|
433 dom = self.__getDomain(domainname) |
|
434 gid = dom.getID() |
|
435 domdir = dom.getDir() |
|
436 if self.__Cfg.dget('domain.force_deletion') or force == 'delall': |
|
437 dom.delete(True, True) |
|
438 elif force == 'deluser': |
|
439 dom.delete(delUser=True) |
|
440 elif force == 'delalias': |
|
441 dom.delete(delAlias=True) |
|
442 else: |
|
443 dom.delete() |
|
444 if self.__Cfg.dget('domain.delete_directory'): |
|
445 self.__domDirDelete(domdir, gid) |
|
446 |
|
447 def domainInfo(self, domainname, details=None): |
|
448 if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full', |
|
449 'relocated', 'detailed']: |
|
450 raise VMMException(_(u'Invalid argument: “%s”') % details, |
|
451 ERR.INVALID_AGUMENT) |
|
452 if details == 'detailed': |
|
453 details = 'full' |
|
454 self.__warnings.append(_(u'\ |
|
455 The keyword “detailed” is deprecated and will be removed in a future release.\n\ |
|
456 Please use the keyword “full” to get full details.')) |
|
457 dom = self.__getDomain(domainname) |
|
458 dominfo = dom.getInfo() |
|
459 if dominfo['domainname'].startswith('xn--'): |
|
460 dominfo['domainname'] += ' (%s)' % ace2idna(dominfo['domainname']) |
|
461 if details is None: |
|
462 return dominfo |
|
463 elif details == 'accounts': |
|
464 return (dominfo, dom.getAccounts()) |
|
465 elif details == 'aliasdomains': |
|
466 return (dominfo, dom.getAliaseNames()) |
|
467 elif details == 'aliases': |
|
468 return (dominfo, dom.getAliases()) |
|
469 elif details == 'relocated': |
|
470 return(dominfo, dom.getRelocated()) |
|
471 else: |
|
472 return (dominfo, dom.getAliaseNames(), dom.getAccounts(), |
|
473 dom.getAliases(), dom.getRelocated()) |
|
474 |
|
475 def aliasDomainAdd(self, aliasname, domainname): |
|
476 """Adds an alias domain to the domain. |
|
477 |
|
478 Keyword arguments: |
|
479 aliasname -- the name of the alias domain (str) |
|
480 domainname -- name of the target domain (str) |
|
481 """ |
|
482 dom = self.__getDomain(domainname) |
|
483 aliasDom = AliasDomain(self.__dbh, aliasname, dom) |
|
484 aliasDom.save() |
|
485 |
|
486 def aliasDomainInfo(self, aliasname): |
|
487 self.__dbConnect() |
|
488 aliasDom = AliasDomain(self.__dbh, aliasname, None) |
|
489 return aliasDom.info() |
|
490 |
|
491 def aliasDomainSwitch(self, aliasname, domainname): |
|
492 """Modifies the target domain of an existing alias domain. |
|
493 |
|
494 Keyword arguments: |
|
495 aliasname -- the name of the alias domain (str) |
|
496 domainname -- name of the new target domain (str) |
|
497 """ |
|
498 dom = self.__getDomain(domainname) |
|
499 aliasDom = AliasDomain(self.__dbh, aliasname, dom) |
|
500 aliasDom.switch() |
|
501 |
|
502 def aliasDomainDelete(self, aliasname): |
|
503 """Deletes the specified alias domain. |
|
504 |
|
505 Keyword arguments: |
|
506 aliasname -- the name of the alias domain (str) |
|
507 """ |
|
508 self.__dbConnect() |
|
509 aliasDom = AliasDomain(self.__dbh, aliasname, None) |
|
510 aliasDom.delete() |
|
511 |
|
512 def domainList(self, pattern=None): |
|
513 from Domain import search |
|
514 like = False |
|
515 if pattern is not None: |
|
516 if pattern.startswith('%') or pattern.endswith('%'): |
|
517 like = True |
|
518 if pattern.startswith('%') and pattern.endswith('%'): |
|
519 domain = pattern[1:-1] |
|
520 elif pattern.startswith('%'): |
|
521 domain = pattern[1:] |
|
522 elif pattern.endswith('%'): |
|
523 domain = pattern[:-1] |
|
524 if not re.match(RE_DOMAIN_SRCH, domain): |
|
525 raise VMMException( |
|
526 _(u"The pattern “%s” contains invalid characters.") % |
|
527 pattern, ERR.DOMAIN_INVALID) |
|
528 self.__dbConnect() |
|
529 return search(self.__dbh, pattern=pattern, like=like) |
|
530 |
|
531 def userAdd(self, emailaddress, password): |
|
532 acc = self.__getAccount(emailaddress, password) |
|
533 if password is None: |
|
534 password = read_pass() |
|
535 acc.setPassword(self.__pwhash(password)) |
|
536 acc.save(self.__Cfg.dget('maildir.name'), |
|
537 self.__Cfg.dget('misc.dovecot_version'), |
|
538 self.__Cfg.dget('account.smtp'), |
|
539 self.__Cfg.dget('account.pop3'), |
|
540 self.__Cfg.dget('account.imap'), |
|
541 self.__Cfg.dget('account.sieve')) |
|
542 self.__mailDirMake(acc.getDir('domain'), acc.getUID(), acc.getGID()) |
|
543 |
|
544 def aliasAdd(self, aliasaddress, targetaddress): |
|
545 alias = self.__getAlias(aliasaddress, targetaddress) |
|
546 alias.save(long(self._postconf.read('virtual_alias_expansion_limit'))) |
|
547 gid = self.__getDomain(alias._dest._domainname).getID() |
|
548 if gid > 0 and not Handler.accountExists(self.__dbh, |
|
549 alias._dest) and not Handler.aliasExists(self.__dbh, |
|
550 alias._dest): |
|
551 self.__warnings.append( |
|
552 _(u"The destination account/alias “%s” doesn't exist.")%\ |
|
553 alias._dest) |
|
554 |
|
555 def userDelete(self, emailaddress, force=None): |
|
556 if force not in [None, 'delalias']: |
|
557 raise VMMException(_(u"Invalid argument: “%s”") % force, |
|
558 ERR.INVALID_AGUMENT) |
|
559 acc = self.__getAccount(emailaddress) |
|
560 uid = acc.getUID() |
|
561 gid = acc.getGID() |
|
562 acc.delete(force) |
|
563 if self.__Cfg.dget('account.delete_directory'): |
|
564 try: |
|
565 self.__userDirDelete(acc.getDir('domain'), uid, gid) |
|
566 except VMMException, e: |
|
567 if e.code() in [ERR.FOUND_DOTS_IN_PATH, |
|
568 ERR.MAILDIR_PERM_MISMATCH, ERR.NO_SUCH_DIRECTORY]: |
|
569 warning = _(u"""\ |
|
570 The account has been successfully deleted from the database. |
|
571 But an error occurred while deleting the following directory: |
|
572 “%(directory)s” |
|
573 Reason: %(reason)s""") % {'directory': acc.getDir('home'),'reason': e.msg()} |
|
574 self.__warnings.append(warning) |
|
575 else: |
|
576 raise e |
|
577 |
|
578 def aliasInfo(self, aliasaddress): |
|
579 alias = self.__getAlias(aliasaddress) |
|
580 return alias.getInfo() |
|
581 |
|
582 def aliasDelete(self, aliasaddress, targetaddress=None): |
|
583 alias = self.__getAlias(aliasaddress, targetaddress) |
|
584 alias.delete() |
|
585 |
|
586 def userInfo(self, emailaddress, details=None): |
|
587 if details not in (None, 'du', 'aliases', 'full'): |
|
588 raise VMMException(_(u'Invalid argument: “%s”') % details, |
|
589 ERR.INVALID_AGUMENT) |
|
590 acc = self.__getAccount(emailaddress) |
|
591 info = acc.getInfo(self.__Cfg.dget('misc.dovecot_version')) |
|
592 if self.__Cfg.dget('account.disk_usage') or details in ('du', 'full'): |
|
593 info['disk usage'] = self.__getDiskUsage('%(maildir)s' % info) |
|
594 if details in (None, 'du'): |
|
595 return info |
|
596 if details in ('aliases', 'full'): |
|
597 return (info, acc.getAliases()) |
|
598 return info |
|
599 |
|
600 def userByID(self, uid): |
|
601 from Handler.Account import getAccountByID |
|
602 self.__dbConnect() |
|
603 return getAccountByID(uid, self.__dbh) |
|
604 |
|
605 def userPassword(self, emailaddress, password): |
|
606 acc = self.__getAccount(emailaddress) |
|
607 if acc.getUID() == 0: |
|
608 raise VMMException(_(u"Account doesn't exist"), ERR.NO_SUCH_ACCOUNT) |
|
609 if password is None: |
|
610 password = read_pass() |
|
611 acc.modify('password', self.__pwhash(password, user=emailaddress)) |
|
612 |
|
613 def userName(self, emailaddress, name): |
|
614 acc = self.__getAccount(emailaddress) |
|
615 acc.modify('name', name) |
|
616 |
|
617 def userTransport(self, emailaddress, transport): |
|
618 acc = self.__getAccount(emailaddress) |
|
619 acc.modify('transport', transport) |
|
620 |
|
621 def userDisable(self, emailaddress, service=None): |
|
622 if service == 'managesieve': |
|
623 service = 'sieve' |
|
624 self.__warnings.append(_(u'\ |
|
625 The service name “managesieve” is deprecated and will be removed\n\ |
|
626 in a future release.\n\ |
|
627 Please use the service name “sieve” instead.')) |
|
628 acc = self.__getAccount(emailaddress) |
|
629 acc.disable(self.__Cfg.dget('misc.dovecot_version'), service) |
|
630 |
|
631 def userEnable(self, emailaddress, service=None): |
|
632 if service == 'managesieve': |
|
633 service = 'sieve' |
|
634 self.__warnings.append(_(u'\ |
|
635 The service name “managesieve” is deprecated and will be removed\n\ |
|
636 in a future release.\n\ |
|
637 Please use the service name “sieve” instead.')) |
|
638 acc = self.__getAccount(emailaddress) |
|
639 acc.enable(self.__Cfg.dget('misc.dovecot_version'), service) |
|
640 |
|
641 def relocatedAdd(self, emailaddress, targetaddress): |
|
642 relocated = self.__getRelocated(emailaddress, targetaddress) |
|
643 relocated.save() |
|
644 |
|
645 def relocatedInfo(self, emailaddress): |
|
646 relocated = self.__getRelocated(emailaddress) |
|
647 return relocated.getInfo() |
|
648 |
|
649 def relocatedDelete(self, emailaddress): |
|
650 relocated = self.__getRelocated(emailaddress) |
|
651 relocated.delete() |
|
652 |
|
653 def __del__(self): |
|
654 if isinstance(self.__dbh, PgSQL.Connection) and self.__dbh._isOpen: |
|
655 self.__dbh.close() |