1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2007 - 2014, Pascal Volk |
|
3 # See COPYING for distribution information. |
|
4 """ |
|
5 VirtualMailManager.alias |
|
6 ~~~~~~~~~~~~~~~~~~~~~~~~ |
|
7 |
|
8 Virtual Mail Manager's Alias class to manage e-mail aliases. |
|
9 """ |
|
10 |
|
11 from VirtualMailManager.domain import get_gid |
|
12 from VirtualMailManager.emailaddress import \ |
|
13 EmailAddress, DestinationEmailAddress as DestAddr |
|
14 from VirtualMailManager.errors import AliasError as AErr |
|
15 from VirtualMailManager.ext.postconf import Postconf |
|
16 from VirtualMailManager.pycompat import all |
|
17 from VirtualMailManager.constants import \ |
|
18 ALIAS_EXCEEDS_EXPANSION_LIMIT, NO_SUCH_ALIAS, NO_SUCH_DOMAIN |
|
19 |
|
20 |
|
21 _ = lambda msg: msg |
|
22 cfg_dget = lambda option: None |
|
23 |
|
24 |
|
25 class Alias(object): |
|
26 """Class to manage e-mail aliases.""" |
|
27 __slots__ = ('_addr', '_dests', '_gid', '_dbh') |
|
28 |
|
29 def __init__(self, dbh, address): |
|
30 assert isinstance(address, EmailAddress) |
|
31 self._addr = address |
|
32 self._dbh = dbh |
|
33 self._gid = get_gid(self._dbh, self._addr.domainname) |
|
34 if not self._gid: |
|
35 raise AErr(_(u"The domain '%s' does not exist.") % |
|
36 self._addr.domainname, NO_SUCH_DOMAIN) |
|
37 self._dests = [] |
|
38 |
|
39 self._load_dests() |
|
40 |
|
41 def _load_dests(self): |
|
42 """Loads all known destination addresses into the _dests list.""" |
|
43 dbc = self._dbh.cursor() |
|
44 dbc.execute('SELECT destination FROM alias WHERE gid = %s AND ' |
|
45 'address = %s ORDER BY destination', |
|
46 (self._gid, self._addr.localpart)) |
|
47 dests = dbc.fetchall() |
|
48 if dbc.rowcount > 0: |
|
49 self._dests.extend(DestAddr(dest[0], self._dbh) for dest in dests) |
|
50 dbc.close() |
|
51 |
|
52 def _check_expansion(self, count_new): |
|
53 """Checks the current expansion limit of the alias.""" |
|
54 postconf = Postconf(cfg_dget('bin.postconf')) |
|
55 limit = long(postconf.read('virtual_alias_expansion_limit')) |
|
56 dcount = len(self._dests) |
|
57 failed = False |
|
58 if dcount == limit or dcount + count_new > limit: |
|
59 failed = True |
|
60 errmsg = _( |
|
61 u"""Cannot add %(count_new)i new destination(s) to alias '%(address)s'. |
|
62 Currently this alias expands into %(count)i/%(limit)i recipients. |
|
63 %(count_new)i additional destination(s) will render this alias unusable. |
|
64 Hint: Increase Postfix' virtual_alias_expansion_limit""") |
|
65 elif dcount > limit: |
|
66 failed = True |
|
67 errmsg = _( |
|
68 u"""Cannot add %(count_new)i new destination(s) to alias '%(address)s'. |
|
69 This alias already exceeds its expansion limit (%(count)i/%(limit)i). |
|
70 So its unusable, all messages addressed to this alias will be bounced. |
|
71 Hint: Delete some destination addresses.""") |
|
72 if failed: |
|
73 raise AErr(errmsg % {'address': self._addr, 'count': dcount, |
|
74 'limit': limit, 'count_new': count_new}, |
|
75 ALIAS_EXCEEDS_EXPANSION_LIMIT) |
|
76 |
|
77 def _delete(self, destinations=None): |
|
78 """Deletes the *destinations* from the alias, if ``destinations`` |
|
79 is not ``None``. If ``destinations`` is None, the alias with all |
|
80 its destination addresses will be deleted. |
|
81 |
|
82 """ |
|
83 dbc = self._dbh.cursor() |
|
84 if not destinations: |
|
85 dbc.execute('DELETE FROM alias WHERE gid = %s AND address = %s', |
|
86 (self._gid, self._addr.localpart)) |
|
87 else: |
|
88 dbc.executemany("DELETE FROM alias WHERE gid = %d AND address = " |
|
89 "'%s' AND destination = %%s" % (self._gid, |
|
90 self._addr.localpart), |
|
91 ((str(dest),) for dest in destinations)) |
|
92 if dbc.rowcount > 0: |
|
93 self._dbh.commit() |
|
94 dbc.close() |
|
95 |
|
96 def __len__(self): |
|
97 """Returns the number of destinations of the alias.""" |
|
98 return len(self._dests) |
|
99 |
|
100 @property |
|
101 def address(self): |
|
102 """The Alias' EmailAddress instance.""" |
|
103 return self._addr |
|
104 |
|
105 def add_destinations(self, destinations, warnings=None): |
|
106 """Adds the `EmailAddress`es from *destinations* list to the |
|
107 destinations of the alias. |
|
108 |
|
109 Destinations, that are already assigned to the alias, will be |
|
110 removed from *destinations*. When done, this method will return |
|
111 a set with all destinations, that were saved in the database. |
|
112 """ |
|
113 destinations = set(destinations) |
|
114 assert destinations and \ |
|
115 all(isinstance(dest, EmailAddress) for dest in destinations) |
|
116 if not warnings is None: |
|
117 assert isinstance(warnings, list) |
|
118 if self._addr in destinations: |
|
119 destinations.remove(self._addr) |
|
120 if not warnings is None: |
|
121 warnings.append(self._addr) |
|
122 duplicates = destinations.intersection(set(self._dests)) |
|
123 if duplicates: |
|
124 destinations.difference_update(set(self._dests)) |
|
125 if not warnings is None: |
|
126 warnings.extend(duplicates) |
|
127 if not destinations: |
|
128 return destinations |
|
129 self._check_expansion(len(destinations)) |
|
130 dbc = self._dbh.cursor() |
|
131 dbc.executemany("INSERT INTO alias (gid, address, destination) " |
|
132 "VALUES (%d, '%s', %%s)" % (self._gid, |
|
133 self._addr.localpart), |
|
134 ((str(destination),) for destination in destinations)) |
|
135 self._dbh.commit() |
|
136 dbc.close() |
|
137 self._dests.extend(destinations) |
|
138 return destinations |
|
139 |
|
140 def del_destinations(self, destinations, warnings=None): |
|
141 """Delete the specified `EmailAddress`es of *destinations* from |
|
142 the alias's destinations. |
|
143 |
|
144 """ |
|
145 destinations = set(destinations) |
|
146 assert destinations and \ |
|
147 all(isinstance(dest, EmailAddress) for dest in destinations) |
|
148 if not warnings is None: |
|
149 assert isinstance(warnings, list) |
|
150 if self._addr in destinations: |
|
151 destinations.remove(self._addr) |
|
152 if not warnings is None: |
|
153 warnings.append(self._addr) |
|
154 if not self._dests: |
|
155 raise AErr(_(u"The alias '%s' does not exist.") % self._addr, |
|
156 NO_SUCH_ALIAS) |
|
157 unknown = destinations.difference(set(self._dests)) |
|
158 if unknown: |
|
159 destinations.intersection_update(set(self._dests)) |
|
160 if not warnings is None: |
|
161 warnings.extend(unknown) |
|
162 if not destinations: |
|
163 raise AErr(_(u"No suitable destinations left to remove from alias " |
|
164 u"'%s'.") % self._addr, NO_SUCH_ALIAS) |
|
165 self._delete(destinations) |
|
166 for destination in destinations: |
|
167 self._dests.remove(destination) |
|
168 |
|
169 def get_destinations(self): |
|
170 """Returns an iterator for all destinations of the alias.""" |
|
171 if not self._dests: |
|
172 raise AErr(_(u"The alias '%s' does not exist.") % self._addr, |
|
173 NO_SUCH_ALIAS) |
|
174 return iter(self._dests) |
|
175 |
|
176 def delete(self): |
|
177 """Deletes the alias with all its destinations.""" |
|
178 if not self._dests: |
|
179 raise AErr(_(u"The alias '%s' does not exist.") % self._addr, |
|
180 NO_SUCH_ALIAS) |
|
181 self._delete() |
|
182 del self._dests[:] |
|
183 |
|
184 del _, cfg_dget |
|