1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2010 - 2014, Pascal Volk |
|
3 # See COPYING for distribution information. |
|
4 """ |
|
5 VirtualMailManager.common |
|
6 ~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
7 |
|
8 Some common functions |
|
9 """ |
|
10 |
|
11 import locale |
|
12 import os |
|
13 import re |
|
14 import stat |
|
15 |
|
16 from VirtualMailManager import ENCODING |
|
17 from VirtualMailManager.constants import INVALID_MAIL_LOCATION, \ |
|
18 NOT_EXECUTABLE, NO_SUCH_BINARY, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED |
|
19 from VirtualMailManager.errors import VMMError |
|
20 |
|
21 VERSION_RE = re.compile(r'^(\d+)\.(\d+)\.(?:(\d+)|(alpha|beta|rc)(\d+))$') |
|
22 |
|
23 _version_level = dict(alpha=0xA, beta=0xB, rc=0xC) |
|
24 _version_cache = {} |
|
25 _ = lambda msg: msg |
|
26 |
|
27 |
|
28 def expand_path(path): |
|
29 """Expands paths, starting with ``.`` or ``~``, to an absolute path.""" |
|
30 if path.startswith('.'): |
|
31 return os.path.abspath(path) |
|
32 if path.startswith('~'): |
|
33 return os.path.expanduser(path) |
|
34 return path |
|
35 |
|
36 |
|
37 def get_unicode(string): |
|
38 """Converts `string` to `unicode`, if necessary.""" |
|
39 if isinstance(string, unicode): |
|
40 return string |
|
41 return unicode(string, ENCODING, 'replace') |
|
42 |
|
43 |
|
44 def lisdir(path): |
|
45 """Checks if `path` is a directory. Doesn't follow symbolic links. |
|
46 Returns bool. |
|
47 """ |
|
48 try: |
|
49 lstat = os.lstat(path) |
|
50 except OSError: |
|
51 return False |
|
52 return stat.S_ISDIR(lstat.st_mode) |
|
53 |
|
54 |
|
55 def exec_ok(binary): |
|
56 """Checks if the `binary` exists and if it is executable. |
|
57 |
|
58 Throws a `VMMError` if the `binary` isn't a file or is not |
|
59 executable. |
|
60 """ |
|
61 binary = expand_path(binary) |
|
62 if not os.path.isfile(binary): |
|
63 raise VMMError(_(u"No such file: '%s'") % get_unicode(binary), |
|
64 NO_SUCH_BINARY) |
|
65 if not os.access(binary, os.X_OK): |
|
66 raise VMMError(_(u"File is not executable: '%s'") % |
|
67 get_unicode(binary), NOT_EXECUTABLE) |
|
68 return binary |
|
69 |
|
70 |
|
71 def human_size(size): |
|
72 """Converts the `size` in bytes in human readable format.""" |
|
73 if not isinstance(size, (long, int)): |
|
74 try: |
|
75 size = long(size) |
|
76 except ValueError: |
|
77 raise TypeError("'size' must be a positive long or int.") |
|
78 if size < 0: |
|
79 raise ValueError("'size' must be a positive long or int.") |
|
80 if size < 1024: |
|
81 return str(size) |
|
82 # TP: abbreviations of gibibyte, tebibyte kibibyte and mebibyte |
|
83 prefix_multiply = ((_(u'TiB'), 1 << 40), (_(u'GiB'), 1 << 30), |
|
84 (_(u'MiB'), 1 << 20), (_(u'KiB'), 1 << 10)) |
|
85 for prefix, multiply in prefix_multiply: |
|
86 if size >= multiply: |
|
87 # TP: e.g.: '%(size)s %(prefix)s' -> '118.30 MiB' |
|
88 return _(u'%(size)s %(prefix)s') % { |
|
89 'size': locale.format('%.2f', float(size) / multiply, |
|
90 True).decode(ENCODING, 'replace'), |
|
91 'prefix': prefix} |
|
92 |
|
93 |
|
94 def size_in_bytes(size): |
|
95 """Converts the string `size` to a long (size in bytes). |
|
96 |
|
97 The string `size` can be suffixed with *b* (bytes), *k* (kilobytes), |
|
98 *M* (megabytes) or *G* (gigabytes). |
|
99 """ |
|
100 if not isinstance(size, basestring) or not size: |
|
101 raise TypeError('size must be a non empty string.') |
|
102 if size[-1].upper() in ('B', 'K', 'M', 'G'): |
|
103 try: |
|
104 num = int(size[:-1]) |
|
105 except ValueError: |
|
106 raise ValueError('Not a valid integer value: %r' % size[:-1]) |
|
107 unit = size[-1].upper() |
|
108 if unit == 'B': |
|
109 return num |
|
110 elif unit == 'K': |
|
111 return num << 10L |
|
112 elif unit == 'M': |
|
113 return num << 20L |
|
114 else: |
|
115 return num << 30L |
|
116 else: |
|
117 try: |
|
118 num = int(size) |
|
119 except ValueError: |
|
120 raise ValueError('Not a valid size value: %r' % size) |
|
121 return num |
|
122 |
|
123 |
|
124 def validate_transport(transport, maillocation): |
|
125 """Checks if the `transport` is usable for the given `maillocation`. |
|
126 |
|
127 Throws a `VMMError` if the chosen `transport` is unable to write |
|
128 messages in the `maillocation`'s mailbox format. |
|
129 |
|
130 Arguments: |
|
131 |
|
132 `transport` : VirtualMailManager.transport.Transport |
|
133 a Transport object |
|
134 `maillocation` : VirtualMailManager.maillocation.MailLocation |
|
135 a MailLocation object |
|
136 """ |
|
137 if transport.transport in ('virtual', 'virtual:') and \ |
|
138 not maillocation.postfix: |
|
139 raise VMMError(_(u"Invalid transport '%(transport)s' for mailbox " |
|
140 u"format '%(mbfmt)s'.") % |
|
141 {'transport': transport.transport, |
|
142 'mbfmt': maillocation.mbformat}, INVALID_MAIL_LOCATION) |
|
143 |
|
144 |
|
145 def version_hex(version_string): |
|
146 """Converts a Dovecot version, e.g.: '1.2.3' or '2.0.beta4', to an int. |
|
147 Raises a `ValueError` if the *version_string* has the wrong™ format. |
|
148 |
|
149 version_hex('1.2.3') -> 270548736 |
|
150 hex(version_hex('1.2.3')) -> '0x10203f00' |
|
151 """ |
|
152 global _version_cache |
|
153 if version_string in _version_cache: |
|
154 return _version_cache[version_string] |
|
155 version = 0 |
|
156 version_mo = VERSION_RE.match(version_string) |
|
157 if not version_mo: |
|
158 raise ValueError('Invalid version string: %r' % version_string) |
|
159 major, minor, patch, level, serial = version_mo.groups() |
|
160 major = int(major) |
|
161 minor = int(minor) |
|
162 if patch: |
|
163 patch = int(patch) |
|
164 if serial: |
|
165 serial = int(serial) |
|
166 |
|
167 if major > 0xFF or minor > 0xFF or \ |
|
168 patch and patch > 0xFF or serial and serial > 0xFF: |
|
169 raise ValueError('Invalid version string: %r' % version_string) |
|
170 |
|
171 version += major << 28 |
|
172 version += minor << 20 |
|
173 if patch: |
|
174 version += patch << 12 |
|
175 version += _version_level.get(level, 0xF) << 8 |
|
176 if serial: |
|
177 version += serial |
|
178 |
|
179 _version_cache[version_string] = version |
|
180 return version |
|
181 |
|
182 |
|
183 def version_str(version): |
|
184 """Converts a Dovecot version previously converted with version_hex back to |
|
185 a string. |
|
186 Raises a `TypeError` if *version* is not an int/long. |
|
187 Raises a `ValueError` if *version* is an incorrect int version. |
|
188 """ |
|
189 global _version_cache |
|
190 if version in _version_cache: |
|
191 return _version_cache[version] |
|
192 if not isinstance(version, (int, long)): |
|
193 raise TypeError('Argument is not a int/long: %r', version) |
|
194 major = (version >> 28) & 0xFF |
|
195 minor = (version >> 20) & 0xFF |
|
196 patch = (version >> 12) & 0xFF |
|
197 level = (version >> 8) & 0x0F |
|
198 serial = version & 0xFF |
|
199 |
|
200 levels = dict(zip(_version_level.values(), _version_level.keys())) |
|
201 if level == 0xF and not serial: |
|
202 version_string = '%u.%u.%u' % (major, minor, patch) |
|
203 elif level in levels and not patch: |
|
204 version_string = '%u.%u.%s%u' % (major, minor, levels[level], serial) |
|
205 else: |
|
206 raise ValueError('Invalid version: %r' % hex(version)) |
|
207 |
|
208 _version_cache[version] = version_string |
|
209 return version_string |
|
210 |
|
211 |
|
212 def format_domain_default(domaindata): |
|
213 """Format info output when the value displayed is the domain default.""" |
|
214 # TP: [domain default] indicates that a user's setting is the same as |
|
215 # configured in the user's domain. |
|
216 # e.g.: [ 0.84%] 42/5,000 [domain default] |
|
217 return _(u'%s [domain default]') % domaindata |
|
218 |
|
219 |
|
220 def search_addresses(dbh, typelimit=None, lpattern=None, llike=False, |
|
221 dpattern=None, dlike=False): |
|
222 """'Search' for addresses by *pattern* in the database. |
|
223 |
|
224 The search is limited by *typelimit*, a bitfield with values TYPE_ACCOUNT, |
|
225 TYPE_ALIAS, TYPE_RELOCATED, or a bitwise OR thereof. If no limit is |
|
226 specified, all types will be searched. |
|
227 |
|
228 *lpattern* may be a local part or a partial local part - starting and/or |
|
229 ending with a '%' sign. When the *lpattern* starts or ends with a '%' sign |
|
230 *llike* has to be `True` to perform a wildcard search. To retrieve all |
|
231 available addresses use the arguments' default values. |
|
232 |
|
233 *dpattern* and *dlike* behave analogously for the domain part of an |
|
234 address, allowing for separate pattern matching: testuser%@example.% |
|
235 |
|
236 The return value of this function is a tuple. The first element is a list |
|
237 of domain IDs sorted alphabetically by the corresponding domain names. The |
|
238 second element is a dictionary indexed by domain ID, holding lists to |
|
239 associated addresses. Each address is itself actually a tuple of address, |
|
240 type, and boolean indicating whether the address stems from an alias |
|
241 domain. |
|
242 """ |
|
243 if typelimit is None: |
|
244 typelimit = TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED |
|
245 queries = [] |
|
246 if typelimit & TYPE_ACCOUNT: |
|
247 queries.append('SELECT gid, local_part, %d AS type FROM users' |
|
248 % TYPE_ACCOUNT) |
|
249 if typelimit & TYPE_ALIAS: |
|
250 queries.append('SELECT DISTINCT gid, address as local_part, ' |
|
251 '%d AS type FROM alias' % TYPE_ALIAS) |
|
252 if typelimit & TYPE_RELOCATED: |
|
253 queries.append('SELECT gid, address as local_part, %d AS type ' |
|
254 'FROM relocated' % TYPE_RELOCATED) |
|
255 sql = "SELECT gid, local_part || '@' || domainname AS address, " |
|
256 sql += 'type, NOT is_primary AS from_aliasdomain FROM (' |
|
257 sql += ' UNION '.join(queries) |
|
258 sql += ') a JOIN domain_name USING (gid)' |
|
259 nextkw = 'WHERE' |
|
260 sqlargs = [] |
|
261 for like, field, pattern in ((dlike, 'domainname', dpattern), |
|
262 (llike, 'local_part', lpattern)): |
|
263 if like: |
|
264 match = 'LIKE' |
|
265 else: |
|
266 if not pattern: |
|
267 continue |
|
268 match = '=' |
|
269 sql += ' %s %s %s %%s' % (nextkw, field, match) |
|
270 sqlargs.append(pattern) |
|
271 nextkw = 'AND' |
|
272 sql += ' ORDER BY domainname, local_part' |
|
273 dbc = dbh.cursor() |
|
274 dbc.execute(sql, sqlargs) |
|
275 result = dbc.fetchall() |
|
276 dbc.close() |
|
277 |
|
278 gids = [] |
|
279 daddrs = {} |
|
280 for gid, address, addrtype, aliasdomain in result: |
|
281 if gid not in daddrs: |
|
282 gids.append(gid) |
|
283 daddrs[gid] = [] |
|
284 daddrs[gid].append((address, addrtype, aliasdomain)) |
|
285 return gids, daddrs |
|
286 |
|
287 del _ |
|