1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2007 - 2010, Pascal Volk |
|
3 # See COPYING for distribution information. |
|
4 |
|
5 """ |
|
6 VirtualMailManager.Account |
|
7 ~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
8 |
|
9 Virtual Mail Manager's Account class to manage e-mail accounts. |
|
10 """ |
|
11 |
|
12 from VirtualMailManager.Domain import Domain |
|
13 from VirtualMailManager.EmailAddress import EmailAddress |
|
14 from VirtualMailManager.Transport import Transport |
|
15 from VirtualMailManager.common import version_str |
|
16 from VirtualMailManager.constants import \ |
|
17 ACCOUNT_EXISTS, ACCOUNT_MISSING_PASSWORD, ALIAS_PRESENT, \ |
|
18 INVALID_ARGUMENT, INVALID_MAIL_LOCATION, NO_SUCH_ACCOUNT, \ |
|
19 NO_SUCH_DOMAIN, UNKNOWN_SERVICE |
|
20 from VirtualMailManager.errors import AccountError as AErr |
|
21 from VirtualMailManager.maillocation import MailLocation |
|
22 from VirtualMailManager.password import pwhash |
|
23 |
|
24 |
|
25 _ = lambda msg: msg |
|
26 cfg_dget = lambda option: None |
|
27 |
|
28 |
|
29 class Account(object): |
|
30 """Class to manage e-mail accounts.""" |
|
31 __slots__ = ('_addr', '_dbh', '_domain', '_mail', '_new', '_passwd', |
|
32 '_transport', '_uid') |
|
33 |
|
34 def __init__(self, dbh, address): |
|
35 """Creates a new Account instance. |
|
36 |
|
37 When an account with the given *address* could be found in the |
|
38 database all relevant data will be loaded. |
|
39 |
|
40 Arguments: |
|
41 |
|
42 `dbh` : pyPgSQL.PgSQL.Connection |
|
43 A database connection for the database access. |
|
44 `address` : VirtualMailManager.EmailAddress.EmailAddress |
|
45 The e-mail address of the (new) Account. |
|
46 """ |
|
47 if not isinstance(address, EmailAddress): |
|
48 raise TypeError("Argument 'address' is not an EmailAddress") |
|
49 self._addr = address |
|
50 self._dbh = dbh |
|
51 self._domain = Domain(self._dbh, self._addr.domainname) |
|
52 if not self._domain.gid: |
|
53 raise AErr(_(u"The domain '%s' doesn't exist.") % |
|
54 self._addr.domainname, NO_SUCH_DOMAIN) |
|
55 self._uid = 0 |
|
56 self._mail = None |
|
57 self._transport = self._domain.transport |
|
58 self._passwd = None |
|
59 self._new = True |
|
60 self._load() |
|
61 |
|
62 def __nonzero__(self): |
|
63 """Returns `True` if the Account is known, `False` if it's new.""" |
|
64 return not self._new |
|
65 |
|
66 def _load(self): |
|
67 """Load 'uid', 'mid' and 'tid' from the database and set _new to |
|
68 `False` - if the user could be found. """ |
|
69 dbc = self._dbh.cursor() |
|
70 dbc.execute('SELECT uid, mid, tid FROM users WHERE gid = %s AND ' |
|
71 'local_part = %s', self._domain.gid, self._addr.localpart) |
|
72 result = dbc.fetchone() |
|
73 dbc.close() |
|
74 if result: |
|
75 self._uid, _mid, _tid = result |
|
76 if _tid != self._transport.tid: |
|
77 self._transport = Transport(self._dbh, tid=_tid) |
|
78 self._mail = MailLocation(self._dbh, mid=_mid) |
|
79 self._new = False |
|
80 |
|
81 def _set_uid(self): |
|
82 """Set the unique ID for the new Account.""" |
|
83 assert self._uid == 0 |
|
84 dbc = self._dbh.cursor() |
|
85 dbc.execute("SELECT nextval('users_uid')") |
|
86 self._uid = dbc.fetchone()[0] |
|
87 dbc.close() |
|
88 |
|
89 def _prepare(self, maillocation): |
|
90 """Check and set different attributes - before we store the |
|
91 information in the database. |
|
92 """ |
|
93 if maillocation.dovecot_version > cfg_dget('misc.dovecot_version'): |
|
94 raise AErr(_(u"The mailbox format '%(mbfmt)s' requires Dovecot " |
|
95 u">= v%(version)s") % {'mbfmt': maillocation.mbformat, |
|
96 'version': version_str(maillocation.dovecot_version)}, |
|
97 INVALID_MAIL_LOCATION) |
|
98 if not maillocation.postfix and \ |
|
99 self._transport.transport.lower() in ('virtual:', 'virtual'): |
|
100 raise AErr(_(u"Invalid transport '%(transport)s' for mailbox " |
|
101 u"format '%(mbfmt)s'") % |
|
102 {'transport': self._transport, |
|
103 'mbfmt': maillocation.mbformat}, INVALID_MAIL_LOCATION) |
|
104 self._mail = maillocation |
|
105 self._set_uid() |
|
106 |
|
107 def _switch_state(self, state, service): |
|
108 """Switch the state of the Account's services on or off. See |
|
109 Account.enable()/Account.disable() for more information.""" |
|
110 self._chk_state() |
|
111 if service not in (None, 'all', 'imap', 'pop3', 'sieve', 'smtp'): |
|
112 raise AErr(_(u"Unknown service: '%s'.") % service, UNKNOWN_SERVICE) |
|
113 if cfg_dget('misc.dovecot_version') >= 0x10200b02: |
|
114 sieve_col = 'sieve' |
|
115 else: |
|
116 sieve_col = 'managesieve' |
|
117 if service in ('smtp', 'pop3', 'imap'): |
|
118 sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (service, state, |
|
119 self._uid) |
|
120 elif service == 'sieve': |
|
121 sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (sieve_col, |
|
122 state, |
|
123 self._uid) |
|
124 else: |
|
125 sql = 'UPDATE users SET smtp = %(s)s, pop3 = %(s)s, imap = %(s)s,\ |
|
126 %(col)s = %(s)s WHERE uid = %(uid)d' % \ |
|
127 {'s': state, 'col': sieve_col, 'uid': self._uid} |
|
128 dbc = self._dbh.cursor() |
|
129 dbc.execute(sql) |
|
130 if dbc.rowcount > 0: |
|
131 self._dbh.commit() |
|
132 dbc.close() |
|
133 |
|
134 def _count_aliases(self): |
|
135 """Count all alias addresses where the destination address is the |
|
136 address of the Account.""" |
|
137 dbc = self._dbh.cursor() |
|
138 sql = "SELECT COUNT(destination) FROM alias WHERE destination = '%s'"\ |
|
139 % self._addr |
|
140 dbc.execute(sql) |
|
141 a_count = dbc.fetchone()[0] |
|
142 dbc.close() |
|
143 return a_count |
|
144 |
|
145 def _chk_state(self): |
|
146 """Raise an AccountError if the Account is new - not yet saved in the |
|
147 database.""" |
|
148 if self._new: |
|
149 raise AErr(_(u"The account '%s' doesn't exist.") % self._addr, |
|
150 NO_SUCH_ACCOUNT) |
|
151 |
|
152 @property |
|
153 def address(self): |
|
154 """The Account's EmailAddress instance.""" |
|
155 return self._addr |
|
156 |
|
157 @property |
|
158 def domain_directory(self): |
|
159 """The directory of the domain the Account belongs to.""" |
|
160 if self._domain: |
|
161 return self._domain.directory |
|
162 return None |
|
163 |
|
164 @property |
|
165 def gid(self): |
|
166 """The Account's group ID.""" |
|
167 if self._domain: |
|
168 return self._domain.gid |
|
169 return None |
|
170 |
|
171 @property |
|
172 def home(self): |
|
173 """The Account's home directory.""" |
|
174 if not self._new: |
|
175 return '%s/%s' % (self._domain.directory, self._uid) |
|
176 return None |
|
177 |
|
178 @property |
|
179 def mail_location(self): |
|
180 """The Account's MailLocation.""" |
|
181 return self._mail |
|
182 |
|
183 @property |
|
184 def uid(self): |
|
185 """The Account's unique ID.""" |
|
186 return self._uid |
|
187 |
|
188 def set_password(self, password): |
|
189 """Set a password for the new Account. |
|
190 |
|
191 If you want to update the password of an existing Account use |
|
192 Account.modify(). |
|
193 |
|
194 Argument: |
|
195 |
|
196 `password` : basestring |
|
197 The password for the new Account. |
|
198 """ |
|
199 if not isinstance(password, basestring) or not password: |
|
200 raise AErr(_(u"Couldn't accept password: '%s'") % password, |
|
201 ACCOUNT_MISSING_PASSWORD) |
|
202 self._passwd = password |
|
203 |
|
204 def set_transport(self, transport): |
|
205 """Set the transport for the new Account. |
|
206 |
|
207 If you want to update the transport of an existing Account use |
|
208 Account.modify(). |
|
209 |
|
210 Argument: |
|
211 |
|
212 `transport` : basestring |
|
213 The string representation of the transport, e.g.: 'dovecot:' |
|
214 """ |
|
215 self._transport = Transport(self._dbh, transport=transport) |
|
216 |
|
217 def enable(self, service=None): |
|
218 """Enable a/all service/s for the Account. |
|
219 |
|
220 Possible values for the *service* are: 'imap', 'pop3', 'sieve' and |
|
221 'smtp'. When all services should be enabled, use 'all' or the |
|
222 default value `None`. |
|
223 |
|
224 Arguments: |
|
225 |
|
226 `service` : basestring |
|
227 The name of a service ('imap', 'pop3', 'smtp', 'sieve'), 'all' |
|
228 or `None`. |
|
229 """ |
|
230 self._switch_state(True, service) |
|
231 |
|
232 def disable(self, service=None): |
|
233 """Disable a/all service/s for the Account. |
|
234 |
|
235 For more information see: Account.enable().""" |
|
236 self._switch_state(False, service) |
|
237 |
|
238 def save(self): |
|
239 """Save the new Account in the database.""" |
|
240 if not self._new: |
|
241 raise AErr(_(u"The account '%s' already exists.") % self._addr, |
|
242 ACCOUNT_EXISTS) |
|
243 if not self._passwd: |
|
244 raise AErr(_(u"No password set for '%s'.") % self._addr, |
|
245 ACCOUNT_MISSING_PASSWORD) |
|
246 if cfg_dget('misc.dovecot_version') >= 0x10200b02: |
|
247 sieve_col = 'sieve' |
|
248 else: |
|
249 sieve_col = 'managesieve' |
|
250 self._prepare(MailLocation(self._dbh, mbfmt=cfg_dget('mailbox.format'), |
|
251 directory=cfg_dget('mailbox.root'))) |
|
252 sql = "INSERT INTO users (local_part, passwd, uid, gid, mid, tid,\ |
|
253 smtp, pop3, imap, %s) VALUES ('%s', '%s', %d, %d, %d, %d, %s, %s, %s, %s)" % ( |
|
254 sieve_col, self._addr.localpart, pwhash(self._passwd, |
|
255 user=self._addr), |
|
256 self._uid, self._domain.gid, self._mail.mid, self._transport.tid, |
|
257 cfg_dget('account.smtp'), cfg_dget('account.pop3'), |
|
258 cfg_dget('account.imap'), cfg_dget('account.sieve')) |
|
259 dbc = self._dbh.cursor() |
|
260 dbc.execute(sql) |
|
261 self._dbh.commit() |
|
262 dbc.close() |
|
263 self._new = False |
|
264 |
|
265 def modify(self, field, value): |
|
266 """Update the Account's *field* to the new *value*. |
|
267 |
|
268 Possible values for *field* are: 'name', 'password' and |
|
269 'transport'. *value* is the *field*'s new value. |
|
270 |
|
271 Arguments: |
|
272 |
|
273 `field` : basestring |
|
274 The attribute name: 'name', 'password' or 'transport' |
|
275 `value` : basestring |
|
276 The new value of the attribute. |
|
277 """ |
|
278 if field not in ('name', 'password', 'transport'): |
|
279 raise AErr(_(u"Unknown field: '%s'") % field, INVALID_ARGUMENT) |
|
280 self._chk_state() |
|
281 dbc = self._dbh.cursor() |
|
282 if field == 'password': |
|
283 dbc.execute('UPDATE users SET passwd = %s WHERE uid = %s', |
|
284 pwhash(value, user=self._addr), self._uid) |
|
285 elif field == 'transport': |
|
286 if value != self._transport.transport: |
|
287 self._transport = Transport(self._dbh, transport=value) |
|
288 dbc.execute('UPDATE users SET tid = %s WHERE uid = %s', |
|
289 self._transport.tid, self._uid) |
|
290 else: |
|
291 dbc.execute('UPDATE users SET name = %s WHERE uid = %s', |
|
292 value, self._uid) |
|
293 if dbc.rowcount > 0: |
|
294 self._dbh.commit() |
|
295 dbc.close() |
|
296 |
|
297 def get_info(self): |
|
298 """Returns a dict with some information about the Account. |
|
299 |
|
300 The keys of the dict are: 'address', 'gid', 'home', 'imap' |
|
301 'mail_location', 'name', 'pop3', 'sieve', 'smtp', transport' and |
|
302 'uid'. |
|
303 """ |
|
304 self._chk_state() |
|
305 if cfg_dget('misc.dovecot_version') >= 0x10200b02: |
|
306 sieve_col = 'sieve' |
|
307 else: |
|
308 sieve_col = 'managesieve' |
|
309 sql = 'SELECT name, smtp, pop3, imap, %s FROM users WHERE uid = %d' % \ |
|
310 (sieve_col, self._uid) |
|
311 dbc = self._dbh.cursor() |
|
312 dbc.execute(sql) |
|
313 info = dbc.fetchone() |
|
314 dbc.close() |
|
315 if info: |
|
316 keys = ('name', 'smtp', 'pop3', 'imap', sieve_col) |
|
317 info = dict(zip(keys, info)) |
|
318 for service in keys[1:]: |
|
319 if info[service]: |
|
320 # TP: A service (pop3/imap) is enabled/usable for a user |
|
321 info[service] = _('enabled') |
|
322 else: |
|
323 # TP: A service (pop3/imap) isn't enabled/usable for a user |
|
324 info[service] = _('disabled') |
|
325 info['address'] = self._addr |
|
326 info['gid'] = self._domain.gid |
|
327 info['home'] = '%s/%s' % (self._domain.directory, self._uid) |
|
328 info['mail_location'] = self._mail.mail_location |
|
329 info['transport'] = self._transport.transport |
|
330 info['uid'] = self._uid |
|
331 return info |
|
332 # nearly impossibleā½ |
|
333 raise AErr(_(u"Couldn't fetch information for account: '%s'") % |
|
334 self._addr, NO_SUCH_ACCOUNT) |
|
335 |
|
336 def get_aliases(self): |
|
337 """Return a list with all alias e-mail addresses, whose destination |
|
338 is the address of the Account.""" |
|
339 self._chk_state() |
|
340 dbc = self._dbh.cursor() |
|
341 dbc.execute("SELECT address ||'@'|| domainname FROM alias, " |
|
342 "domain_name WHERE destination = %s AND domain_name.gid = " |
|
343 "alias.gid AND domain_name.is_primary ORDER BY address", |
|
344 str(self._addr)) |
|
345 addresses = dbc.fetchall() |
|
346 dbc.close() |
|
347 aliases = [] |
|
348 if addresses: |
|
349 aliases = [alias[0] for alias in addresses] |
|
350 return aliases |
|
351 |
|
352 def delete(self, delalias=False): |
|
353 """Delete the Account from the database. |
|
354 |
|
355 Argument: |
|
356 |
|
357 `delalias` : bool |
|
358 if *delalias* is `True`, all aliases, which points to the Account, |
|
359 will be also deleted. If there are aliases and *delalias* is |
|
360 `False`, an AccountError will be raised. |
|
361 """ |
|
362 assert isinstance(delalias, bool) |
|
363 self._chk_state() |
|
364 dbc = self._dbh.cursor() |
|
365 if delalias: |
|
366 dbc.execute('DELETE FROM users WHERE uid = %s', self._uid) |
|
367 # delete also all aliases where the destination address is the same |
|
368 # as for this account. |
|
369 dbc.execute("DELETE FROM alias WHERE destination = %s", |
|
370 str(self._addr)) |
|
371 self._dbh.commit() |
|
372 else: # check first for aliases |
|
373 a_count = self._count_aliases() |
|
374 if a_count > 0: |
|
375 dbc.close() |
|
376 raise AErr(_(u"There are %(count)d aliases with the " |
|
377 u"destination address '%(address)s'.") % |
|
378 {'count': a_count, 'address': self._addr}, |
|
379 ALIAS_PRESENT) |
|
380 dbc.execute('DELETE FROM users WHERE uid = %s', self._uid) |
|
381 self._dbh.commit() |
|
382 dbc.close() |
|
383 self._new = True |
|
384 self._uid = 0 |
|
385 self._addr = self._dbh = self._domain = self._passwd = None |
|
386 self._mail = self._transport = None |
|
387 |
|
388 |
|
389 def get_account_by_uid(uid, dbh): |
|
390 """Search an Account by its UID. |
|
391 |
|
392 This function returns a dict (keys: 'address', 'gid' and 'uid'), if an |
|
393 Account with the given *uid* exists. |
|
394 |
|
395 Argument: |
|
396 |
|
397 `uid` : long |
|
398 The Account unique ID. |
|
399 `dbh` : pyPgSQL.PgSQL.Connection |
|
400 a database connection for the database access. |
|
401 """ |
|
402 try: |
|
403 uid = long(uid) |
|
404 except ValueError: |
|
405 raise AErr(_(u'UID must be an int/long.'), INVALID_ARGUMENT) |
|
406 if uid < 1: |
|
407 raise AErr(_(u'UID must be greater than 0.'), INVALID_ARGUMENT) |
|
408 dbc = dbh.cursor() |
|
409 dbc.execute("SELECT local_part||'@'|| domain_name.domainname AS address, " |
|
410 "uid, users.gid FROM users LEFT JOIN domain_name ON " |
|
411 "(domain_name.gid = users.gid AND is_primary) WHERE uid = %s", |
|
412 uid) |
|
413 info = dbc.fetchone() |
|
414 dbc.close() |
|
415 if not info: |
|
416 raise AErr(_(u"There is no account with the UID '%d'.") % uid, |
|
417 NO_SUCH_ACCOUNT) |
|
418 info = dict(zip(('address', 'uid', 'gid'), info)) |
|
419 return info |
|
420 |
|
421 del _, cfg_dget |
|