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