|
1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2007 - 2010, Pascal Volk |
|
3 # See COPYING for distribution information. |
|
4 """ |
|
5 VirtualMailManager.domain |
|
6 ~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
7 |
|
8 Virtual Mail Manager's Domain class to manage e-mail domains. |
|
9 """ |
|
10 |
|
11 import os |
|
12 import re |
|
13 from random import choice |
|
14 |
|
15 from VirtualMailManager.constants import \ |
|
16 ACCOUNT_AND_ALIAS_PRESENT, ACCOUNT_PRESENT, ALIAS_PRESENT, \ |
|
17 DOMAIN_ALIAS_EXISTS, DOMAIN_EXISTS, DOMAIN_INVALID, DOMAIN_TOO_LONG, \ |
|
18 NO_SUCH_DOMAIN |
|
19 from VirtualMailManager.errors import DomainError as DomErr |
|
20 from VirtualMailManager.transport import Transport |
|
21 |
|
22 |
|
23 MAILDIR_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz' |
|
24 RE_DOMAIN = re.compile(r"^(?:[a-z0-9-]{1,63}\.){1,}[a-z]{2,6}$") |
|
25 _ = lambda msg: msg |
|
26 |
|
27 |
|
28 class Domain(object): |
|
29 """Class to manage e-mail domains.""" |
|
30 __slots__ = ('_directory', '_gid', '_name', '_transport', '_dbh', '_new') |
|
31 |
|
32 def __init__(self, dbh, domainname): |
|
33 """Creates a new Domain instance. |
|
34 |
|
35 Loads all relevant data from the database, if the domain could be |
|
36 found. To create a new domain call the methods set_directory() and |
|
37 set_transport() before save(). |
|
38 |
|
39 A DomainError will be thrown when the *domainname* is the name of |
|
40 an alias domain. |
|
41 |
|
42 Arguments: |
|
43 |
|
44 `dbh` : pyPgSQL.PgSQL.Connection |
|
45 a database connection for the database access |
|
46 `domainname` : basestring |
|
47 The name of the domain |
|
48 """ |
|
49 self._name = check_domainname(domainname) |
|
50 self._dbh = dbh |
|
51 self._gid = 0 |
|
52 self._transport = None |
|
53 self._directory = None |
|
54 self._new = True |
|
55 self._load() |
|
56 |
|
57 def _load(self): |
|
58 """Load information from the database and checks if the domain name |
|
59 is the primary one. |
|
60 |
|
61 Raises a DomainError if Domain._name isn't the primary name of the |
|
62 domain. |
|
63 """ |
|
64 dbc = self._dbh.cursor() |
|
65 dbc.execute('SELECT dd.gid, tid, domaindir, is_primary FROM ' |
|
66 'domain_data dd, domain_name dn WHERE domainname = %s AND ' |
|
67 'dn.gid = dd.gid', self._name) |
|
68 result = dbc.fetchone() |
|
69 dbc.close() |
|
70 if result: |
|
71 if not result[3]: |
|
72 raise DomErr(_(u"The domain '%s' is an alias domain.") % |
|
73 self._name, DOMAIN_ALIAS_EXISTS) |
|
74 self._gid, self._directory = result[0], result[2] |
|
75 self._transport = Transport(self._dbh, tid=result[1]) |
|
76 self._new = False |
|
77 |
|
78 def _set_gid(self): |
|
79 """Sets the ID of the domain - if not set yet.""" |
|
80 assert self._gid == 0 |
|
81 dbc = self._dbh.cursor() |
|
82 dbc.execute("SELECT nextval('domain_gid')") |
|
83 self._gid = dbc.fetchone()[0] |
|
84 dbc.close() |
|
85 |
|
86 def _has(self, what): |
|
87 """Checks if aliases or accounts are assigned to the domain. |
|
88 |
|
89 If there are assigned accounts or aliases True will be returned, |
|
90 otherwise False will be returned. |
|
91 |
|
92 Argument: |
|
93 |
|
94 `what` : basestring |
|
95 "alias" or "users" |
|
96 """ |
|
97 assert what in ('alias', 'users') |
|
98 dbc = self._dbh.cursor() |
|
99 if what == 'users': |
|
100 dbc.execute("SELECT count(gid) FROM users WHERE gid=%s", self._gid) |
|
101 else: |
|
102 dbc.execute("SELECT count(gid) FROM alias WHERE gid=%s", self._gid) |
|
103 count = dbc.fetchone() |
|
104 dbc.close() |
|
105 return count[0] > 0 |
|
106 |
|
107 def _chk_delete(self, deluser, delalias): |
|
108 """Checks dependencies for deletion. |
|
109 |
|
110 Arguments: |
|
111 deluser -- ignore available accounts (bool) |
|
112 delalias -- ignore available aliases (bool) |
|
113 """ |
|
114 if not deluser: |
|
115 hasuser = self._has('users') |
|
116 else: |
|
117 hasuser = False |
|
118 if not delalias: |
|
119 hasalias = self._has('alias') |
|
120 else: |
|
121 hasalias = False |
|
122 if hasuser and hasalias: |
|
123 raise DomErr(_(u'There are accounts and aliases.'), |
|
124 ACCOUNT_AND_ALIAS_PRESENT) |
|
125 elif hasuser: |
|
126 raise DomErr(_(u'There are accounts.'), ACCOUNT_PRESENT) |
|
127 elif hasalias: |
|
128 raise DomErr(_(u'There are aliases.'), ALIAS_PRESENT) |
|
129 |
|
130 def _chk_state(self): |
|
131 """Throws a DomainError if the Domain is new - not saved in the |
|
132 database.""" |
|
133 if self._new: |
|
134 raise DomErr(_(u"The domain '%s' doesn't exist.") % self._name, |
|
135 NO_SUCH_DOMAIN) |
|
136 |
|
137 @property |
|
138 def gid(self): |
|
139 """The GID of the Domain.""" |
|
140 return self._gid |
|
141 |
|
142 @property |
|
143 def name(self): |
|
144 """The Domain's name.""" |
|
145 return self._name |
|
146 |
|
147 @property |
|
148 def directory(self): |
|
149 """The Domain's directory.""" |
|
150 return self._directory |
|
151 |
|
152 def set_directory(self, basedir): |
|
153 """Set the path value of the Domain's directory, inside *basedir*. |
|
154 |
|
155 Argument: |
|
156 |
|
157 `basedir` : basestring |
|
158 The base directory of all domains |
|
159 """ |
|
160 assert self._new and self._directory is None |
|
161 self._set_gid() |
|
162 self._directory = os.path.join(basedir, choice(MAILDIR_CHARS), |
|
163 str(self._gid)) |
|
164 |
|
165 @property |
|
166 def transport(self): |
|
167 """The Domain's transport.""" |
|
168 return self._transport |
|
169 |
|
170 def set_transport(self, transport): |
|
171 """Set the transport for the new Domain. |
|
172 |
|
173 Argument: |
|
174 |
|
175 `transport` : VirtualMailManager.Transport |
|
176 The transport of the new Domain |
|
177 """ |
|
178 assert self._new and isinstance(transport, Transport) |
|
179 self._transport = transport |
|
180 |
|
181 def save(self): |
|
182 """Stores the new domain in the database.""" |
|
183 if not self._new: |
|
184 raise DomErr(_(u"The domain '%s' already exists.") % self._name, |
|
185 DOMAIN_EXISTS) |
|
186 assert self._directory is not None and self._transport is not None |
|
187 dbc = self._dbh.cursor() |
|
188 dbc.execute("INSERT INTO domain_data VALUES (%s, %s, %s)", self._gid, |
|
189 self._transport.tid, self._directory) |
|
190 dbc.execute("INSERT INTO domain_name VALUES (%s, %s, %s)", self._name, |
|
191 self._gid, True) |
|
192 self._dbh.commit() |
|
193 dbc.close() |
|
194 self._new = False |
|
195 |
|
196 def delete(self, deluser=False, delalias=False): |
|
197 """Deletes the domain. |
|
198 |
|
199 Arguments: |
|
200 |
|
201 `deluser` : bool |
|
202 force deletion of all available accounts, default `False` |
|
203 `delalias` : bool |
|
204 force deletion of all available aliases, default `False` |
|
205 """ |
|
206 self._chk_state() |
|
207 self._chk_delete(deluser, delalias) |
|
208 dbc = self._dbh.cursor() |
|
209 for tbl in ('alias', 'users', 'relocated', 'domain_name', |
|
210 'domain_data'): |
|
211 dbc.execute("DELETE FROM %s WHERE gid = %d" % (tbl, self._gid)) |
|
212 self._dbh.commit() |
|
213 dbc.close() |
|
214 self._gid = 0 |
|
215 self._directory = self._transport = None |
|
216 self._new = True |
|
217 |
|
218 def update_transport(self, transport, force=False): |
|
219 """Sets a new transport for the Domain. |
|
220 |
|
221 If *force* is `True` the new *transport* will be assigned to all |
|
222 existing accounts. Otherwise the *transport* will be only used for |
|
223 accounts created from now on. |
|
224 |
|
225 Arguments: |
|
226 |
|
227 `transport` : VirtualMailManager.Transport |
|
228 the new transport |
|
229 `force` : bool |
|
230 enforce new transport setting for all accounts, default `False` |
|
231 """ |
|
232 self._chk_state() |
|
233 assert isinstance(transport, Transport) |
|
234 if transport == self._transport: |
|
235 return |
|
236 dbc = self._dbh.cursor() |
|
237 dbc.execute("UPDATE domain_data SET tid = %s WHERE gid = %s", |
|
238 transport.tid, self._gid) |
|
239 if dbc.rowcount > 0: |
|
240 self._dbh.commit() |
|
241 if force: |
|
242 dbc.execute("UPDATE users SET tid = %s WHERE gid = %s", |
|
243 transport.tid, self._gid) |
|
244 if dbc.rowcount > 0: |
|
245 self._dbh.commit() |
|
246 dbc.close() |
|
247 self._transport = transport |
|
248 |
|
249 def get_info(self): |
|
250 """Returns a dictionary with information about the domain.""" |
|
251 self._chk_state() |
|
252 dbc = self._dbh.cursor() |
|
253 dbc.execute('SELECT gid, domainname, transport, domaindir, ' |
|
254 'aliasdomains accounts, aliases, relocated FROM ' |
|
255 'vmm_domain_info WHERE gid = %s', self._gid) |
|
256 info = dbc.fetchone() |
|
257 dbc.close() |
|
258 keys = ('gid', 'domainname', 'transport', 'domaindir', 'aliasdomains', |
|
259 'accounts', 'aliases', 'relocated') |
|
260 return dict(zip(keys, info)) |
|
261 |
|
262 def get_accounts(self): |
|
263 """Returns a list with all accounts of the domain.""" |
|
264 self._chk_state() |
|
265 dbc = self._dbh.cursor() |
|
266 dbc.execute('SELECT local_part from users where gid = %s ORDER BY ' |
|
267 'local_part', self._gid) |
|
268 users = dbc.fetchall() |
|
269 dbc.close() |
|
270 accounts = [] |
|
271 if users: |
|
272 addr = u'@'.join |
|
273 _dom = self._name |
|
274 accounts = [addr((account[0], _dom)) for account in users] |
|
275 return accounts |
|
276 |
|
277 def get_aliases(self): |
|
278 """Returns a list with all aliases e-mail addresses of the domain.""" |
|
279 self._chk_state() |
|
280 dbc = self._dbh.cursor() |
|
281 dbc.execute('SELECT DISTINCT address FROM alias WHERE gid = %s ORDER ' |
|
282 'BY address', self._gid) |
|
283 addresses = dbc.fetchall() |
|
284 dbc.close() |
|
285 aliases = [] |
|
286 if addresses: |
|
287 addr = u'@'.join |
|
288 _dom = self._name |
|
289 aliases = [addr((alias[0], _dom)) for alias in addresses] |
|
290 return aliases |
|
291 |
|
292 def get_relocated(self): |
|
293 """Returns a list with all addresses of relocated users.""" |
|
294 self._chk_state() |
|
295 dbc = self._dbh.cursor() |
|
296 dbc.execute('SELECT address FROM relocated WHERE gid = %s ORDER BY ' |
|
297 'address', self._gid) |
|
298 addresses = dbc.fetchall() |
|
299 dbc.close() |
|
300 relocated = [] |
|
301 if addresses: |
|
302 addr = u'@'.join |
|
303 _dom = self._name |
|
304 relocated = [addr((address[0], _dom)) for address in addresses] |
|
305 return relocated |
|
306 |
|
307 def get_aliase_names(self): |
|
308 """Returns a list with all alias domain names of the domain.""" |
|
309 self._chk_state() |
|
310 dbc = self._dbh.cursor() |
|
311 dbc.execute('SELECT domainname FROM domain_name WHERE gid = %s AND ' |
|
312 'NOT is_primary ORDER BY domainname', self._gid) |
|
313 anames = dbc.fetchall() |
|
314 dbc.close() |
|
315 aliasdomains = [] |
|
316 if anames: |
|
317 aliasdomains = [aname[0] for aname in anames] |
|
318 return aliasdomains |
|
319 |
|
320 |
|
321 def check_domainname(domainname): |
|
322 """Returns the validated domain name `domainname`. |
|
323 |
|
324 Throws an `DomainError`, if the domain name is too long or doesn't |
|
325 look like a valid domain name (label.label.label). |
|
326 |
|
327 """ |
|
328 if not RE_DOMAIN.match(domainname): |
|
329 domainname = domainname.encode('idna') |
|
330 if len(domainname) > 255: |
|
331 raise DomErr(_(u'The domain name is too long'), DOMAIN_TOO_LONG) |
|
332 if not RE_DOMAIN.match(domainname): |
|
333 raise DomErr(_(u"The domain name '%s' is invalid") % domainname, |
|
334 DOMAIN_INVALID) |
|
335 return domainname |
|
336 |
|
337 |
|
338 def get_gid(dbh, domainname): |
|
339 """Returns the group id of the domain *domainname*. |
|
340 |
|
341 If the domain couldn't be found in the database 0 will be returned. |
|
342 """ |
|
343 domainname = check_domainname(domainname) |
|
344 dbc = dbh.cursor() |
|
345 dbc.execute('SELECT gid FROM domain_name WHERE domainname=%s', domainname) |
|
346 gid = dbc.fetchone() |
|
347 dbc.close() |
|
348 if gid: |
|
349 return gid[0] |
|
350 return 0 |
|
351 |
|
352 |
|
353 def search(dbh, pattern=None, like=False): |
|
354 """'Search' for domains by *pattern* in the database. |
|
355 |
|
356 *pattern* may be a domain name or a partial domain name - starting |
|
357 and/or ending with a '%' sign. When the *pattern* starts or ends with |
|
358 a '%' sign *like* has to be `True` to perform a wildcard search. |
|
359 To retrieve all available domains use the arguments' default values. |
|
360 |
|
361 This function returns a tuple with a list and a dict: (order, domains). |
|
362 The order list contains the domains' gid, alphabetical sorted by the |
|
363 primary domain name. The domains dict's keys are the gids of the |
|
364 domains. The value of item is a list. The first list element contains |
|
365 the primary domain name or `None`. The elements [1:] contains the |
|
366 names of alias domains. |
|
367 |
|
368 Arguments: |
|
369 |
|
370 `pattern` : basestring |
|
371 a (partial) domain name (starting and/or ending with a "%" sign) |
|
372 `like` : bool |
|
373 should be `True` when *pattern* starts/ends with a "%" sign |
|
374 """ |
|
375 if pattern and not like: |
|
376 pattern = check_domainname(pattern) |
|
377 sql = 'SELECT gid, domainname, is_primary FROM domain_name' |
|
378 if pattern: |
|
379 if like: |
|
380 sql += " WHERE domainname LIKE '%s'" % pattern |
|
381 else: |
|
382 sql += " WHERE domainname = '%s'" % pattern |
|
383 sql += ' ORDER BY is_primary DESC, domainname' |
|
384 dbc = dbh.cursor() |
|
385 dbc.execute(sql) |
|
386 result = dbc.fetchall() |
|
387 dbc.close() |
|
388 |
|
389 gids = [domain[0] for domain in result if domain[2]] |
|
390 domains = {} |
|
391 for gid, domain, is_primary in result: |
|
392 if is_primary: |
|
393 if not gid in domains: |
|
394 domains[gid] = [domain] |
|
395 else: |
|
396 domains[gid].insert(0, domain) |
|
397 else: |
|
398 if gid in gids: |
|
399 if gid in domains: |
|
400 domains[gid].append(domain) |
|
401 else: |
|
402 domains[gid] = [domain] |
|
403 else: |
|
404 gids.append(gid) |
|
405 domains[gid] = [None, domain] |
|
406 return gids, domains |
|
407 |
|
408 del _ |