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