9 """ |
9 """ |
10 |
10 |
11 from VirtualMailManager.Domain import Domain |
11 from VirtualMailManager.Domain import Domain |
12 from VirtualMailManager.EmailAddress import EmailAddress |
12 from VirtualMailManager.EmailAddress import EmailAddress |
13 from VirtualMailManager.Transport import Transport |
13 from VirtualMailManager.Transport import Transport |
|
14 from VirtualMailManager.common import version_str |
14 from VirtualMailManager.constants.ERROR import \ |
15 from VirtualMailManager.constants.ERROR import \ |
15 ACCOUNT_EXISTS, ACCOUNT_MISSING_PASSWORD, ALIAS_PRESENT, \ |
16 ACCOUNT_EXISTS, ACCOUNT_MISSING_PASSWORD, ALIAS_PRESENT, \ |
16 INVALID_AGUMENT, NO_SUCH_ACCOUNT, NO_SUCH_DOMAIN, \ |
17 INVALID_AGUMENT, INVALID_MAIL_LOCATION, NO_SUCH_ACCOUNT, NO_SUCH_DOMAIN, \ |
17 UNKNOWN_MAILLOCATION_NAME, UNKNOWN_SERVICE |
18 UNKNOWN_SERVICE |
18 from VirtualMailManager.errors import AccountError as AErr |
19 from VirtualMailManager.errors import AccountError as AErr |
19 from VirtualMailManager.maillocation import MailLocation, known_format |
20 from VirtualMailManager.maillocation import MailLocation |
20 from VirtualMailManager.pycompat import all |
21 from VirtualMailManager.password import pwhash |
21 |
22 |
22 |
23 |
23 _ = lambda msg: msg |
24 _ = lambda msg: msg |
|
25 cfg_dget = lambda option: None |
24 |
26 |
25 |
27 |
26 class Account(object): |
28 class Account(object): |
27 """Class to manage e-mail accounts.""" |
29 """Class to manage e-mail accounts.""" |
28 __slots__ = ('_addr', '_domain', '_mid', '_new', '_passwd', '_tid', '_uid', |
30 __slots__ = ('_addr', '_dbh', '_domain', '_mid', '_new', '_passwd', |
29 '_dbh') |
31 '_transport', '_uid') |
30 |
32 |
31 def __init__(self, dbh, address): |
33 def __init__(self, dbh, address): |
32 """Creates a new Account instance. |
34 """Creates a new Account instance. |
33 |
35 |
34 When an account with the given *address* could be found in the |
36 When an account with the given *address* could be found in the |
77 self._uid = dbc.fetchone()[0] |
85 self._uid = dbc.fetchone()[0] |
78 dbc.close() |
86 dbc.close() |
79 |
87 |
80 def _prepare(self, maillocation): |
88 def _prepare(self, maillocation): |
81 """Check and set different attributes - before we store the |
89 """Check and set different attributes - before we store the |
82 information in the database.""" |
90 information in the database. |
83 if not known_format(maillocation): |
91 """ |
84 raise AErr(_(u'Unknown mail_location mailbox format: %r') % |
92 if maillocation.dovecot_version > cfg_dget('misc.dovecot_version'): |
85 maillocation, UNKNOWN_MAILLOCATION_NAME) |
93 raise AErr(_("The mail_location prefix '%(prefix)s' requires \ |
86 self._mid = MailLocation(format=maillocation).mid |
94 Dovecot >= v%(version)s") % {'prefix': maillocation.prefix, |
87 if not self._tid: |
95 'version': version_str(maillocation.dovecot_version)}, |
88 self._tid = self._domain.transport.tid |
96 INVALID_MAIL_LOCATION) |
|
97 if not maillocation.postfix and \ |
|
98 self._transport.transport.lower() in ('virtual:', 'virtual'): |
|
99 raise AErr(_(u"Invalid transport '%(transport)s' for mail_location\ |
|
100 prefix '%(prefix)s'") % {'transport': self._transport, |
|
101 'prefix': maillocation.prefix}, |
|
102 INVALID_MAIL_LOCATION) |
|
103 self._mid = maillocation.mid |
89 self._set_uid() |
104 self._set_uid() |
90 |
105 |
91 def _switch_state(self, state, dcvers, service): |
106 def _switch_state(self, state, service): |
92 """Switch the state of the Account's services on or off. See |
107 """Switch the state of the Account's services on or off. See |
93 Account.enable()/Account.disable() for more information.""" |
108 Account.enable()/Account.disable() for more information.""" |
94 self._chk_state() |
109 self._chk_state() |
95 if service not in (None, 'all', 'imap', 'pop3', 'sieve', 'smtp'): |
110 if service not in (None, 'all', 'imap', 'pop3', 'sieve', 'smtp'): |
96 raise AErr(_(u"Unknown service: '%s'.") % service, UNKNOWN_SERVICE) |
111 raise AErr(_(u"Unknown service: '%s'.") % service, UNKNOWN_SERVICE) |
97 if dcvers >= 0x10200b02: |
112 if cfg_dget('misc.dovecot_version') >= 0x10200b02: |
98 sieve_col = 'sieve' |
113 sieve_col = 'sieve' |
99 else: |
114 else: |
100 sieve_col = 'managesieve' |
115 sieve_col = 'managesieve' |
101 if service in ('smtp', 'pop3', 'imap'): |
116 if service in ('smtp', 'pop3', 'imap'): |
102 sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (service, state, |
117 sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (service, state, |
179 Argument: |
204 Argument: |
180 |
205 |
181 `transport` : basestring |
206 `transport` : basestring |
182 The string representation of the transport, e.g.: 'dovecot:' |
207 The string representation of the transport, e.g.: 'dovecot:' |
183 """ |
208 """ |
184 self._tid = Transport(self._dbh, transport=transport).tid |
209 self._transport = Transport(self._dbh, transport=transport) |
185 |
210 |
186 def enable(self, dcvers, service=None): |
211 def enable(self, service=None): |
187 """Enable a/all service/s for the Account. |
212 """Enable a/all service/s for the Account. |
188 |
213 |
189 Possible values for the *service* are: 'imap', 'pop3', 'sieve' and |
214 Possible values for the *service* are: 'imap', 'pop3', 'sieve' and |
190 'smtp'. When all services should be enabled, use 'all' or the |
215 'smtp'. When all services should be enabled, use 'all' or the |
191 default value `None`. |
216 default value `None`. |
192 |
217 |
193 Arguments: |
218 Arguments: |
194 |
219 |
195 `dcvers` : int |
|
196 The concatenated major and minor version number from |
|
197 `dovecot --version`. |
|
198 `service` : basestring |
220 `service` : basestring |
199 The name of a service ('imap', 'pop3', 'smtp', 'sieve'), 'all' |
221 The name of a service ('imap', 'pop3', 'smtp', 'sieve'), 'all' |
200 or `None`. |
222 or `None`. |
201 """ |
223 """ |
202 self._switch_state(True, dcvers, service) |
224 self._switch_state(True, service) |
203 |
225 |
204 def disable(self, dcvers, service=None): |
226 def disable(self, service=None): |
205 """Disable a/all service/s for the Account. |
227 """Disable a/all service/s for the Account. |
206 |
228 |
207 For more information see: Account.enable().""" |
229 For more information see: Account.enable().""" |
208 self._switch_state(False, dcvers, service) |
230 self._switch_state(False, service) |
209 |
231 |
210 def save(self, maillocation, dcvers, smtp, pop3, imap, sieve): |
232 def save(self): |
211 """Save the new Account in the database. |
233 """Save the new Account in the database.""" |
212 |
|
213 Arguments: |
|
214 |
|
215 `maillocation` : basestring |
|
216 The mailbox format of the mail_location: 'maildir', 'mbox', |
|
217 'dbox' or 'mdbox'. |
|
218 `dcvers` : int |
|
219 The concatenated major and minor version number from |
|
220 `dovecot --version`. |
|
221 `smtp, pop3, imap, sieve` : bool |
|
222 Indicates if the user of the Account should be able to use this |
|
223 services. |
|
224 """ |
|
225 if not self._new: |
234 if not self._new: |
226 raise AErr(_(u"The account '%s' already exists.") % self._addr, |
235 raise AErr(_(u"The account '%s' already exists.") % self._addr, |
227 ACCOUNT_EXISTS) |
236 ACCOUNT_EXISTS) |
228 if not self._passwd: |
237 if not self._passwd: |
229 raise AErr(_(u"No password set for '%s'.") % self._addr, |
238 raise AErr(_(u"No password set for '%s'.") % self._addr, |
230 ACCOUNT_MISSING_PASSWORD) |
239 ACCOUNT_MISSING_PASSWORD) |
231 assert all(isinstance(service, bool) for service in (smtp, pop3, imap, |
240 if cfg_dget('misc.dovecot_version') >= 0x10200b02: |
232 sieve)) |
|
233 if dcvers >= 0x10200b02: |
|
234 sieve_col = 'sieve' |
241 sieve_col = 'sieve' |
235 else: |
242 else: |
236 sieve_col = 'managesieve' |
243 sieve_col = 'managesieve' |
237 self._prepare(maillocation) |
244 self._prepare(MailLocation(format=cfg_dget('mailbox.format'))) |
238 sql = "INSERT INTO users (local_part, passwd, uid, gid, mid, tid,\ |
245 sql = "INSERT INTO users (local_part, passwd, uid, gid, mid, tid,\ |
239 smtp, pop3, imap, %s) VALUES ('%s', '%s', %d, %d, %d, %d, %s, %s, %s, %s)" % ( |
246 smtp, pop3, imap, %s) VALUES ('%s', '%s', %d, %d, %d, %d, %s, %s, %s, %s)" % ( |
240 sieve_col, self._addr.localpart, self._passwd, self._uid, |
247 sieve_col, self._addr.localpart, pwhash(self._passwd), self._uid, |
241 self._domain.gid, self._mid, self._tid, smtp, pop3, imap, sieve) |
248 self._domain.gid, self._mid, self._transport.tid, |
|
249 cfg_dget('account.smtp'), cfg_dget('account.pop3'), |
|
250 cfg_dget('account.imap'), cfg_dget('account.sieve')) |
242 dbc = self._dbh.cursor() |
251 dbc = self._dbh.cursor() |
243 dbc.execute(sql) |
252 dbc.execute(sql) |
244 self._dbh.commit() |
253 self._dbh.commit() |
245 dbc.close() |
254 dbc.close() |
246 self._new = False |
255 self._new = False |
247 |
256 |
248 def modify(self, field, value): |
257 def modify(self, field, value): |
249 """Update the Account's *field* to the new *value*. |
258 """Update the Account's *field* to the new *value*. |
250 |
259 |
251 Possible values for *filed* are: 'name', 'password' and |
260 Possible values for *field* are: 'name', 'password' and |
252 'transport'. *value* is the *field*'s new value. |
261 'transport'. *value* is the *field*'s new value. |
253 |
262 |
254 Arguments: |
263 Arguments: |
255 |
264 |
256 `field` : basestring |
265 `field` : basestring |
257 The attribute name: 'name', 'password' or 'transport' |
266 The attribute name: 'name', 'password' or 'transport' |
258 `value` : basestring |
267 `value` : basestring |
259 The new value of the attribute. The password is expected as a |
268 The new value of the attribute. |
260 hashed password string. |
|
261 """ |
269 """ |
262 if field not in ('name', 'password', 'transport'): |
270 if field not in ('name', 'password', 'transport'): |
263 raise AErr(_(u"Unknown field: '%s'") % field, INVALID_AGUMENT) |
271 raise AErr(_(u"Unknown field: '%s'") % field, INVALID_AGUMENT) |
264 self._chk_state() |
272 self._chk_state() |
265 dbc = self._dbh.cursor() |
273 dbc = self._dbh.cursor() |
266 if field == 'password': |
274 if field == 'password': |
267 dbc.execute('UPDATE users SET passwd = %s WHERE uid = %s', |
275 dbc.execute('UPDATE users SET passwd = %s WHERE uid = %s', |
268 value, self._uid) |
276 pwhash(value), self._uid) |
269 elif field == 'transport': |
277 elif field == 'transport': |
270 self._tid = Transport(self._dbh, transport=value).tid |
278 if value != self._transport.transport: |
271 dbc.execute('UPDATE users SET tid = %s WHERE uid = %s', |
279 self._transport = Transport(self._dbh, transport=value) |
272 self._tid, self._uid) |
280 dbc.execute('UPDATE users SET tid = %s WHERE uid = %s', |
|
281 self._transport.tid, self._uid) |
273 else: |
282 else: |
274 dbc.execute('UPDATE users SET name = %s WHERE uid = %s', |
283 dbc.execute('UPDATE users SET name = %s WHERE uid = %s', |
275 value, self._uid) |
284 value, self._uid) |
276 if dbc.rowcount > 0: |
285 if dbc.rowcount > 0: |
277 self._dbh.commit() |
286 self._dbh.commit() |
278 dbc.close() |
287 dbc.close() |
279 |
288 |
280 def get_info(self, dcvers): |
289 def get_info(self): |
281 """Returns a dict with some information about the Account. |
290 """Returns a dict with some information about the Account. |
282 |
291 |
283 The keys of the dict are: 'address', 'gid', 'home', 'imap' |
292 The keys of the dict are: 'address', 'gid', 'home', 'imap' |
284 'mail_location', 'name', 'pop3', 'sieve', 'smtp', transport' and |
293 'mail_location', 'name', 'pop3', 'sieve', 'smtp', transport' and |
285 'uid'. |
294 'uid'. |
286 |
|
287 Argument: |
|
288 |
|
289 `dcvers` : int |
|
290 The concatenated major and minor version number from |
|
291 `dovecot --version`. |
|
292 """ |
295 """ |
293 self._chk_state() |
296 self._chk_state() |
294 if dcvers >= 0x10200b02: |
297 if cfg_dget('misc.dovecot_version') >= 0x10200b02: |
295 sieve_col = 'sieve' |
298 sieve_col = 'sieve' |
296 else: |
299 else: |
297 sieve_col = 'managesieve' |
300 sieve_col = 'managesieve' |
298 sql = 'SELECT name, uid, gid, mid, tid, smtp, pop3, imap, %s\ |
301 sql = 'SELECT name, smtp, pop3, imap, %s FROM users WHERE uid = %d' % \ |
299 FROM users WHERE uid = %d' % (sieve_col, self._uid) |
302 (sieve_col, self._uid) |
300 dbc = self._dbh.cursor() |
303 dbc = self._dbh.cursor() |
301 dbc.execute(sql) |
304 dbc.execute(sql) |
302 info = dbc.fetchone() |
305 info = dbc.fetchone() |
303 dbc.close() |
306 dbc.close() |
304 if info: |
307 if info: |
305 keys = ('name', 'uid', 'gid', 'mid', 'transport', 'smtp', |
308 keys = ('name', 'smtp', 'pop3', 'imap', sieve_col) |
306 'pop3', 'imap', sieve_col) |
|
307 info = dict(zip(keys, info)) |
309 info = dict(zip(keys, info)) |
308 for service in ('smtp', 'pop3', 'imap', sieve_col): |
310 for service in keys[1:]: |
309 if info[service]: |
311 if info[service]: |
310 # TP: A service (pop3/imap) is enabled/usable for a user |
312 # TP: A service (pop3/imap) is enabled/usable for a user |
311 info[service] = _('enabled') |
313 info[service] = _('enabled') |
312 else: |
314 else: |
313 # TP: A service (pop3/imap) isn't enabled/usable for a user |
315 # TP: A service (pop3/imap) isn't enabled/usable for a user |
314 info[service] = _('disabled') |
316 info[service] = _('disabled') |
315 info['address'] = self._addr |
317 info['address'] = self._addr |
316 info['home'] = '%s/%s' % (self._domain.directory, info['uid']) |
318 info['gid'] = self._domain.gid |
317 info['mail_location'] = MailLocation(mid=info['mid']).mail_location |
319 info['home'] = '%s/%s' % (self._domain.directory, self._uid) |
318 if info['transport'] == self._domain.transport.tid: |
320 info['mail_location'] = MailLocation(mid=self._mid).mail_location |
319 info['transport'] = self._domain.transport.transport |
321 info['transport'] = self._transport.transport |
320 else: |
322 info['uid'] = self._uid |
321 info['transport'] = Transport(self._dbh, |
|
322 tid=info['transport']).transport |
|
323 del info['mid'] |
|
324 return info |
323 return info |
325 # nearly impossibleā½ |
324 # nearly impossibleā½ |
326 raise AErr(_(u"Couldn't fetch information for account: '%s'") \ |
325 raise AErr(_(u"Couldn't fetch information for account: '%s'") \ |
327 % self._addr, NO_SUCH_ACCOUNT) |
326 % self._addr, NO_SUCH_ACCOUNT) |
328 |
327 |
339 aliases = [] |
338 aliases = [] |
340 if addresses: |
339 if addresses: |
341 aliases = [alias[0] for alias in addresses] |
340 aliases = [alias[0] for alias in addresses] |
342 return aliases |
341 return aliases |
343 |
342 |
344 def delete(self, delalias): |
343 def delete(self, delalias=False): |
345 """Delete the Account from the database. |
344 """Delete the Account from the database. |
346 |
345 |
347 Argument: |
346 Argument: |
348 |
347 |
349 `delalias` : basestring |
348 `delalias` : bool |
350 if the values of delalias is 'delalias', all aliases, which |
349 if *delalias* is `True`, all aliases, which points to the Account, |
351 points to the Account, will be also deleted.""" |
350 will be also deleted. If there are aliases and *delalias* is |
|
351 `False`, an AccountError will be raised. |
|
352 """ |
|
353 assert isinstance(delalias, bool) |
352 self._chk_state() |
354 self._chk_state() |
353 dbc = self._dbh.cursor() |
355 dbc = self._dbh.cursor() |
354 if delalias == 'delalias': |
356 if delalias: |
355 dbc.execute('DELETE FROM users WHERE uid= %s', self._uid) |
357 dbc.execute('DELETE FROM users WHERE uid = %s', self._uid) |
356 # delete also all aliases where the destination address is the same |
358 # delete also all aliases where the destination address is the same |
357 # as for this account. |
359 # as for this account. |
358 dbc.execute("DELETE FROM alias WHERE destination = %s", |
360 dbc.execute("DELETE FROM alias WHERE destination = %s", |
359 str(self._addr)) |
361 str(self._addr)) |
360 self._dbh.commit() |
362 self._dbh.commit() |
361 else: # check first for aliases |
363 else: # check first for aliases |
362 a_count = self._count_aliases() |
364 a_count = self._count_aliases() |
363 if a_count == 0: |
365 if a_count > 0: |
364 dbc.execute('DELETE FROM users WHERE uid = %s', self._uid) |
|
365 self._dbh.commit() |
|
366 else: |
|
367 dbc.close() |
366 dbc.close() |
368 raise AErr(_(u"There are %(count)d aliases with the \ |
367 raise AErr(_(u"There are %(count)d aliases with the \ |
369 destination address '%(address)s'.") % \ |
368 destination address '%(address)s'.") % \ |
370 {'count': a_count, 'address': self._addr}, |
369 {'count': a_count, 'address': self._addr}, |
371 ALIAS_PRESENT) |
370 ALIAS_PRESENT) |
372 dbc.close() |
371 dbc.execute('DELETE FROM users WHERE uid = %s', self._uid) |
|
372 self._dbh.commit() |
|
373 dbc.close() |
|
374 self._new = True |
|
375 self._uid = self._mid = 0 |
|
376 self._addr = self._dbh = self._domain = self._passwd = None |
|
377 self._transport = None |
373 |
378 |
374 |
379 |
375 def get_account_by_uid(uid, dbh): |
380 def get_account_by_uid(uid, dbh): |
376 """Search an Account by its UID. |
381 """Search an Account by its UID. |
377 |
382 |