1 # -*- coding: UTF-8 -*- |
1 # -*- coding: UTF-8 -*- |
2 # Copyright (c) 2007 - 2010, Pascal Volk |
2 # Copyright (c) 2007 - 2010, Pascal Volk |
3 # See COPYING for distribution information. |
3 # See COPYING for distribution information. |
4 |
4 |
5 """The main class for vmm.""" |
5 """ |
6 |
6 VirtualMailManager.Handler |
7 |
7 |
8 from encodings.idna import ToASCII, ToUnicode |
8 A wrapper class. It wraps round all other classes and does some |
9 from getpass import getpass |
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 |
10 from shutil import rmtree |
18 from shutil import rmtree |
11 from subprocess import Popen, PIPE |
19 from subprocess import Popen, PIPE |
12 |
20 |
13 from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net |
21 from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net |
14 |
22 |
15 from __main__ import os, re, ENCODING, ERR, w_std |
23 import VirtualMailManager.constants.ERROR as ERR |
16 from ext.Postconf import Postconf |
24 from VirtualMailManager import ENCODING, ace2idna, exec_ok, read_pass |
17 from Account import Account |
25 from VirtualMailManager.Account import Account |
18 from Alias import Alias |
26 from VirtualMailManager.Alias import Alias |
19 from AliasDomain import AliasDomain |
27 from VirtualMailManager.AliasDomain import AliasDomain |
20 from Config import Config as Cfg |
28 from VirtualMailManager.Config import Config as Cfg |
21 from Domain import Domain |
29 from VirtualMailManager.Domain import Domain |
22 from EmailAddress import EmailAddress |
30 from VirtualMailManager.EmailAddress import EmailAddress |
23 from Exceptions import * |
31 from VirtualMailManager.Exceptions import * |
24 from Relocated import Relocated |
32 from VirtualMailManager.Relocated import Relocated |
|
33 from VirtualMailManager.ext.Postconf import Postconf |
25 |
34 |
26 SALTCHARS = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' |
35 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-\.]+$""" |
36 RE_DOMAIN_SRCH = """^[a-z0-9-\.]+$""" |
30 RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]""" |
37 RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]""" |
31 RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$""" |
38 RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$""" |
32 |
39 |
33 class VirtualMailManager(object): |
40 class Handler(object): |
34 """The main class for vmm""" |
41 """Wrapper class to simplify the access on all the stuff from |
|
42 VirtualMailManager""" |
|
43 # TODO: accept a LazyConfig object as argument |
35 __slots__ = ('__Cfg', '__cfgFileName', '__dbh', '__scheme', '__warnings', |
44 __slots__ = ('__Cfg', '__cfgFileName', '__dbh', '__scheme', '__warnings', |
36 '_postconf') |
45 '_postconf') |
37 def __init__(self): |
46 def __init__(self): |
38 """Creates a new VirtualMailManager instance. |
47 """Creates a new Handler instance. |
39 Throws a VMMNotRootException if your uid is greater 0. |
48 Throws a VMMNotRootException if your uid is greater 0. |
40 """ |
49 """ |
41 self.__cfgFileName = '' |
50 self.__cfgFileName = '' |
42 self.__warnings = [] |
51 self.__warnings = [] |
43 self.__Cfg = None |
52 self.__Cfg = None |
91 elif not os.path.isdir(basedir): |
100 elif not os.path.isdir(basedir): |
92 raise VMMException(_(u'“%s” is not a directory.\n\ |
101 raise VMMException(_(u'“%s” is not a directory.\n\ |
93 (vmm.cfg: section "misc", option "base_directory")') % |
102 (vmm.cfg: section "misc", option "base_directory")') % |
94 basedir, ERR.NO_SUCH_DIRECTORY) |
103 basedir, ERR.NO_SUCH_DIRECTORY) |
95 for opt, val in self.__Cfg.items('bin'): |
104 for opt, val in self.__Cfg.items('bin'): |
96 if not os.path.exists(val): |
105 try: |
97 raise VMMException(_(u'“%(binary)s” doesn\'t exist.\n\ |
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\ |
98 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt}, |
111 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt}, |
99 ERR.NO_SUCH_BINARY) |
112 ERR.NO_SUCH_BINARY) |
100 elif not os.access(val, os.X_OK): |
113 elif code is ERR.NOT_EXECUTABLE: |
101 raise VMMException(_(u'“%(binary)s” is not executable.\n\ |
114 raise VMMException(_(u'“%(binary)s” is not executable.\n\ |
102 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt}, |
115 (vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt}, |
103 ERR.NOT_EXECUTABLE) |
116 ERR.NOT_EXECUTABLE) |
|
117 else: |
|
118 raise |
104 |
119 |
105 def __dbConnect(self): |
120 def __dbConnect(self): |
106 """Creates a pyPgSQL.PgSQL.connection instance.""" |
121 """Creates a pyPgSQL.PgSQL.connection instance.""" |
107 if self.__dbh is None or not self.__dbh._isOpen: |
122 if self.__dbh is None or (isinstance(self.__dbh, PgSQL.Connection) and |
|
123 not self.__dbh._isOpen): |
108 try: |
124 try: |
109 self.__dbh = PgSQL.connect( |
125 self.__dbh = PgSQL.connect( |
110 database=self.__Cfg.dget('database.name'), |
126 database=self.__Cfg.dget('database.name'), |
111 user=self.__Cfg.pget('database.user'), |
127 user=self.__Cfg.pget('database.user'), |
112 host=self.__Cfg.dget('database.host'), |
128 host=self.__Cfg.dget('database.host'), |
116 dbc.execute("SET NAMES 'UTF8'") |
132 dbc.execute("SET NAMES 'UTF8'") |
117 dbc.close() |
133 dbc.close() |
118 except PgSQL.libpq.DatabaseError, e: |
134 except PgSQL.libpq.DatabaseError, e: |
119 raise VMMException(str(e), ERR.DATABASE_ERROR) |
135 raise VMMException(str(e), ERR.DATABASE_ERROR) |
120 |
136 |
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): |
137 def _exists(dbh, query): |
158 dbc = dbh.cursor() |
138 dbc = dbh.cursor() |
159 dbc.execute(query) |
139 dbc.execute(query) |
160 gid = dbc.fetchone() |
140 gid = dbc.fetchone() |
161 dbc.close() |
141 dbc.close() |
167 |
147 |
168 def accountExists(dbh, address): |
148 def accountExists(dbh, address): |
169 sql = "SELECT gid FROM users WHERE gid = (SELECT gid FROM domain_name\ |
149 sql = "SELECT gid FROM users WHERE gid = (SELECT gid FROM domain_name\ |
170 WHERE domainname = '%s') AND local_part = '%s'" % (address._domainname, |
150 WHERE domainname = '%s') AND local_part = '%s'" % (address._domainname, |
171 address._localpart) |
151 address._localpart) |
172 return VirtualMailManager._exists(dbh, sql) |
152 return Handler._exists(dbh, sql) |
173 accountExists = staticmethod(accountExists) |
153 accountExists = staticmethod(accountExists) |
174 |
154 |
175 def aliasExists(dbh, address): |
155 def aliasExists(dbh, address): |
176 sql = "SELECT DISTINCT gid FROM alias WHERE gid = (SELECT gid FROM\ |
156 sql = "SELECT DISTINCT gid FROM alias WHERE gid = (SELECT gid FROM\ |
177 domain_name WHERE domainname = '%s') AND address = '%s'" %\ |
157 domain_name WHERE domainname = '%s') AND address = '%s'" %\ |
178 (address._domainname, address._localpart) |
158 (address._domainname, address._localpart) |
179 return VirtualMailManager._exists(dbh, sql) |
159 return Handler._exists(dbh, sql) |
180 aliasExists = staticmethod(aliasExists) |
160 aliasExists = staticmethod(aliasExists) |
181 |
161 |
182 def relocatedExists(dbh, address): |
162 def relocatedExists(dbh, address): |
183 sql = "SELECT gid FROM relocated WHERE gid = (SELECT gid FROM\ |
163 sql = "SELECT gid FROM relocated WHERE gid = (SELECT gid FROM\ |
184 domain_name WHERE domainname = '%s') AND address = '%s'" %\ |
164 domain_name WHERE domainname = '%s') AND address = '%s'" %\ |
185 (address._domainname, address._localpart) |
165 (address._domainname, address._localpart) |
186 return VirtualMailManager._exists(dbh, sql) |
166 return Handler._exists(dbh, sql) |
187 relocatedExists = staticmethod(relocatedExists) |
167 relocatedExists = staticmethod(relocatedExists) |
188 |
168 |
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 |
169 |
213 def __getAccount(self, address, password=None): |
170 def __getAccount(self, address, password=None): |
214 self.__dbConnect() |
171 self.__dbConnect() |
215 address = EmailAddress(address) |
172 address = EmailAddress(address) |
216 if not password is None: |
173 if not password is None: |
498 The keyword “detailed” is deprecated and will be removed in a future release.\n\ |
455 The keyword “detailed” is deprecated and will be removed in a future release.\n\ |
499 Please use the keyword “full” to get full details.')) |
456 Please use the keyword “full” to get full details.')) |
500 dom = self.__getDomain(domainname) |
457 dom = self.__getDomain(domainname) |
501 dominfo = dom.getInfo() |
458 dominfo = dom.getInfo() |
502 if dominfo['domainname'].startswith('xn--'): |
459 if dominfo['domainname'].startswith('xn--'): |
503 dominfo['domainname'] += ' (%s)'\ |
460 dominfo['domainname'] += ' (%s)' % ace2idna(dominfo['domainname']) |
504 % VirtualMailManager.ace2idna(dominfo['domainname']) |
|
505 if details is None: |
461 if details is None: |
506 return dominfo |
462 return dominfo |
507 elif details == 'accounts': |
463 elif details == 'accounts': |
508 return (dominfo, dom.getAccounts()) |
464 return (dominfo, dom.getAccounts()) |
509 elif details == 'aliasdomains': |
465 elif details == 'aliasdomains': |
573 return search(self.__dbh, pattern=pattern, like=like) |
529 return search(self.__dbh, pattern=pattern, like=like) |
574 |
530 |
575 def userAdd(self, emailaddress, password): |
531 def userAdd(self, emailaddress, password): |
576 acc = self.__getAccount(emailaddress, password) |
532 acc = self.__getAccount(emailaddress, password) |
577 if password is None: |
533 if password is None: |
578 password = self._readpass() |
534 password = read_pass() |
579 acc.setPassword(self.__pwhash(password)) |
535 acc.setPassword(self.__pwhash(password)) |
580 acc.save(self.__Cfg.dget('maildir.name'), |
536 acc.save(self.__Cfg.dget('maildir.name'), |
581 self.__Cfg.dget('misc.dovecot_version'), |
537 self.__Cfg.dget('misc.dovecot_version'), |
582 self.__Cfg.dget('account.smtp'), |
538 self.__Cfg.dget('account.smtp'), |
583 self.__Cfg.dget('account.pop3'), |
539 self.__Cfg.dget('account.pop3'), |
587 |
543 |
588 def aliasAdd(self, aliasaddress, targetaddress): |
544 def aliasAdd(self, aliasaddress, targetaddress): |
589 alias = self.__getAlias(aliasaddress, targetaddress) |
545 alias = self.__getAlias(aliasaddress, targetaddress) |
590 alias.save(long(self._postconf.read('virtual_alias_expansion_limit'))) |
546 alias.save(long(self._postconf.read('virtual_alias_expansion_limit'))) |
591 gid = self.__getDomain(alias._dest._domainname).getID() |
547 gid = self.__getDomain(alias._dest._domainname).getID() |
592 if gid > 0 and not VirtualMailManager.accountExists(self.__dbh, |
548 if gid > 0 and not Handler.accountExists(self.__dbh, |
593 alias._dest) and not VirtualMailManager.aliasExists(self.__dbh, |
549 alias._dest) and not Handler.aliasExists(self.__dbh, |
594 alias._dest): |
550 alias._dest): |
595 self.__warnings.append( |
551 self.__warnings.append( |
596 _(u"The destination account/alias “%s” doesn't exist.")%\ |
552 _(u"The destination account/alias “%s” doesn't exist.")%\ |
597 alias._dest) |
553 alias._dest) |
598 |
554 |
640 if details in ('aliases', 'full'): |
596 if details in ('aliases', 'full'): |
641 return (info, acc.getAliases()) |
597 return (info, acc.getAliases()) |
642 return info |
598 return info |
643 |
599 |
644 def userByID(self, uid): |
600 def userByID(self, uid): |
645 from Account import getAccountByID |
601 from Handler.Account import getAccountByID |
646 self.__dbConnect() |
602 self.__dbConnect() |
647 return getAccountByID(uid, self.__dbh) |
603 return getAccountByID(uid, self.__dbh) |
648 |
604 |
649 def userPassword(self, emailaddress, password): |
605 def userPassword(self, emailaddress, password): |
650 acc = self.__getAccount(emailaddress) |
606 acc = self.__getAccount(emailaddress) |
651 if acc.getUID() == 0: |
607 if acc.getUID() == 0: |
652 raise VMMException(_(u"Account doesn't exist"), ERR.NO_SUCH_ACCOUNT) |
608 raise VMMException(_(u"Account doesn't exist"), ERR.NO_SUCH_ACCOUNT) |
653 if password is None: |
609 if password is None: |
654 password = self._readpass() |
610 password = read_pass() |
655 acc.modify('password', self.__pwhash(password, user=emailaddress)) |
611 acc.modify('password', self.__pwhash(password, user=emailaddress)) |
656 |
612 |
657 def userName(self, emailaddress, name): |
613 def userName(self, emailaddress, name): |
658 acc = self.__getAccount(emailaddress) |
614 acc = self.__getAccount(emailaddress) |
659 acc.modify('name', name) |
615 acc.modify('name', name) |