|
1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2007 - 2010, Pascal Volk |
|
3 # See COPYING for distribution information. |
|
4 """ |
|
5 VirtualMailManager.config |
|
6 ~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
7 |
|
8 VMM's configuration module for simplified configuration access. |
|
9 """ |
|
10 |
|
11 import re |
|
12 |
|
13 from ConfigParser import \ |
|
14 Error, MissingSectionHeaderError, NoOptionError, NoSectionError, \ |
|
15 ParsingError, RawConfigParser |
|
16 from cStringIO import StringIO# TODO: move interactive stff to cli |
|
17 |
|
18 from VirtualMailManager.common import exec_ok, get_unicode, is_dir, version_hex |
|
19 from VirtualMailManager.constants import CONF_ERROR |
|
20 from VirtualMailManager.errors import ConfigError, VMMError |
|
21 from VirtualMailManager.maillocation import known_format |
|
22 from VirtualMailManager.password import verify_scheme as _verify_scheme |
|
23 |
|
24 |
|
25 _ = lambda msg: msg |
|
26 |
|
27 |
|
28 class BadOptionError(Error): |
|
29 """Raised when a option isn't in the format 'section.option'.""" |
|
30 pass |
|
31 |
|
32 |
|
33 class ConfigValueError(Error): |
|
34 """Raised when creating or validating of new values fails.""" |
|
35 pass |
|
36 |
|
37 |
|
38 class NoDefaultError(Error): |
|
39 """Raised when the requested option has no default value.""" |
|
40 |
|
41 def __init__(self, section, option): |
|
42 Error.__init__(self, 'Option %r in section %r has no default value' % |
|
43 (option, section)) |
|
44 |
|
45 |
|
46 class LazyConfig(RawConfigParser): |
|
47 """The **lazy** derivate of the `RawConfigParser`. |
|
48 |
|
49 There are two additional getters: |
|
50 |
|
51 `pget()` |
|
52 The polymorphic getter, which returns a option's value with the |
|
53 appropriate type. |
|
54 `dget()` |
|
55 Like `LazyConfig.pget()`, but returns the option's default, from |
|
56 `LazyConfig._cfg['sectionname']['optionname'].default`, if the |
|
57 option is not configured in a ini-like configuration file. |
|
58 |
|
59 `set()` differs from `RawConfigParser`'s `set()` method. `set()` |
|
60 takes the `section` and `option` arguments combined to a single |
|
61 string in the form "section.option". |
|
62 """ |
|
63 |
|
64 def __init__(self): |
|
65 RawConfigParser.__init__(self) |
|
66 self._modified = False |
|
67 # sample _cfg dict. Create your own in your derived class. |
|
68 self._cfg = { |
|
69 'sectionname': { |
|
70 'optionname': LazyConfigOption(int, 1, self.getint), |
|
71 } |
|
72 } |
|
73 |
|
74 def bool_new(self, value): |
|
75 """Converts the string `value` into a `bool` and returns it. |
|
76 |
|
77 | '1', 'on', 'yes' and 'true' will become `True` |
|
78 | '0', 'off', 'no' and 'false' will become `False` |
|
79 |
|
80 Throws a `ConfigValueError` for all other values, except bools. |
|
81 """ |
|
82 if isinstance(value, bool): |
|
83 return value |
|
84 if value.lower() in self._boolean_states: |
|
85 return self._boolean_states[value.lower()] |
|
86 else: |
|
87 raise ConfigValueError(_(u"Not a boolean: '%s'") % |
|
88 get_unicode(value)) |
|
89 |
|
90 def getboolean(self, section, option): |
|
91 """Returns the boolean value of the option, in the given |
|
92 section. |
|
93 |
|
94 For a boolean True, the value must be set to '1', 'on', 'yes', |
|
95 'true' or True. For a boolean False, the value must set to '0', |
|
96 'off', 'no', 'false' or False. |
|
97 If the option has another value assigned this method will raise |
|
98 a ValueError. |
|
99 """ |
|
100 # if the setting was modified it may be still a boolean value lets see |
|
101 tmp = self.get(section, option) |
|
102 if isinstance(tmp, bool): |
|
103 return tmp |
|
104 if not tmp.lower() in self._boolean_states: |
|
105 raise ValueError('Not a boolean: %s' % tmp) |
|
106 return self._boolean_states[tmp.lower()] |
|
107 |
|
108 def _get_section_option(self, section_option): |
|
109 """splits ``section_option`` (section.option) in two parts and |
|
110 returns them as list ``[section, option]``, if: |
|
111 |
|
112 * it likes the format of ``section_option`` |
|
113 * the ``section`` is known |
|
114 * the ``option`` is known |
|
115 |
|
116 Else one of the following exceptions will be thrown: |
|
117 |
|
118 * `BadOptionError` |
|
119 * `NoSectionError` |
|
120 * `NoOptionError` |
|
121 """ |
|
122 sect_opt = section_option.lower().split('.') |
|
123 # TODO: cache it |
|
124 if len(sect_opt) != 2: # do we need a regexp to check the format? |
|
125 raise BadOptionError(_(u"Bad format: '%s' - expected: " |
|
126 u"section.option") % |
|
127 get_unicode(section_option)) |
|
128 if not sect_opt[0] in self._cfg: |
|
129 raise NoSectionError(sect_opt[0]) |
|
130 if not sect_opt[1] in self._cfg[sect_opt[0]]: |
|
131 raise NoOptionError(sect_opt[1], sect_opt[0]) |
|
132 return sect_opt |
|
133 |
|
134 def items(self, section): |
|
135 """returns an iterable that returns key, value ``tuples`` from |
|
136 the given ``section``. |
|
137 """ |
|
138 if section in self._sections: # check if the section was parsed |
|
139 sect = self._sections[section] |
|
140 elif not section in self._cfg: |
|
141 raise NoSectionError(section) |
|
142 else: |
|
143 return ((k, self._cfg[section][k].default) \ |
|
144 for k in self._cfg[section].iterkeys()) |
|
145 # still here? Get defaults and merge defaults with configured setting |
|
146 defaults = dict((k, self._cfg[section][k].default) \ |
|
147 for k in self._cfg[section].iterkeys()) |
|
148 defaults.update(sect) |
|
149 if '__name__' in defaults: |
|
150 del defaults['__name__'] |
|
151 return defaults.iteritems() |
|
152 |
|
153 def dget(self, option): |
|
154 """Returns the value of the `option`. |
|
155 |
|
156 If the option could not be found in the configuration file, the |
|
157 configured default value, from ``LazyConfig._cfg`` will be |
|
158 returned. |
|
159 |
|
160 Arguments: |
|
161 |
|
162 `option` : string |
|
163 the configuration option in the form "section.option" |
|
164 |
|
165 Throws a `NoDefaultError`, if no default value was passed to |
|
166 `LazyConfigOption.__init__()` for the `option`. |
|
167 """ |
|
168 section, option = self._get_section_option(option) |
|
169 try: |
|
170 return self._cfg[section][option].getter(section, option) |
|
171 except (NoSectionError, NoOptionError): |
|
172 if not self._cfg[section][option].default is None: # may be False |
|
173 return self._cfg[section][option].default |
|
174 else: |
|
175 raise NoDefaultError(section, option) |
|
176 |
|
177 def pget(self, option): |
|
178 """Returns the value of the `option`.""" |
|
179 section, option = self._get_section_option(option) |
|
180 return self._cfg[section][option].getter(section, option) |
|
181 |
|
182 def set(self, option, value): |
|
183 """Set the `value` of the `option`. |
|
184 |
|
185 Throws a `ValueError` if `value` couldn't be converted using |
|
186 `LazyConfigOption.cls`. |
|
187 """ |
|
188 # pylint: disable=W0221 |
|
189 # @pylint: _L A Z Y_ |
|
190 section, option = self._get_section_option(option) |
|
191 val = self._cfg[section][option].cls(value) |
|
192 if self._cfg[section][option].validate: |
|
193 val = self._cfg[section][option].validate(val) |
|
194 if not RawConfigParser.has_section(self, section): |
|
195 self.add_section(section) |
|
196 RawConfigParser.set(self, section, option, val) |
|
197 self._modified = True |
|
198 |
|
199 def has_section(self, section): |
|
200 """Checks if `section` is a known configuration section.""" |
|
201 return section.lower() in self._cfg |
|
202 |
|
203 def has_option(self, option): |
|
204 """Checks if the option (section.option) is a known |
|
205 configuration option. |
|
206 """ |
|
207 # pylint: disable=W0221 |
|
208 # @pylint: _L A Z Y_ |
|
209 try: |
|
210 self._get_section_option(option) |
|
211 return True |
|
212 except(BadOptionError, NoSectionError, NoOptionError): |
|
213 return False |
|
214 |
|
215 def sections(self): |
|
216 """Returns an iterator object for all configuration sections.""" |
|
217 return self._cfg.iterkeys() |
|
218 |
|
219 |
|
220 class LazyConfigOption(object): |
|
221 """A simple container class for configuration settings. |
|
222 |
|
223 `LazyConfigOption` instances are required by `LazyConfig` instances, |
|
224 and instances of classes derived from `LazyConfig`, like the |
|
225 `Config` class. |
|
226 """ |
|
227 __slots__ = ('__cls', '__default', '__getter', '__validate') |
|
228 |
|
229 def __init__(self, cls, default, getter, validate=None): |
|
230 """Creates a new `LazyConfigOption` instance. |
|
231 |
|
232 Arguments: |
|
233 |
|
234 `cls` : type |
|
235 The class/type of the option's value |
|
236 `default` |
|
237 Default value of the option. Use ``None`` if the option should |
|
238 not have a default value. |
|
239 `getter` : callable |
|
240 A method's name of `RawConfigParser` and derived classes, to |
|
241 get a option's value, e.g. `self.getint`. |
|
242 `validate` : NoneType or a callable |
|
243 None or any method, that takes one argument, in order to |
|
244 check the value, when `LazyConfig.set()` is called. |
|
245 """ |
|
246 self.__cls = cls |
|
247 if not default is None: # enforce the type of the default value |
|
248 self.__default = self.__cls(default) |
|
249 else: |
|
250 self.__default = default |
|
251 if not callable(getter): |
|
252 raise TypeError('getter has to be a callable, got a %r' % |
|
253 getter.__class__.__name__) |
|
254 self.__getter = getter |
|
255 if validate and not callable(validate): |
|
256 raise TypeError('validate has to be callable or None, got a %r' % |
|
257 validate.__class__.__name__) |
|
258 self.__validate = validate |
|
259 |
|
260 @property |
|
261 def cls(self): |
|
262 """The class of the option's value e.g. `str`, `unicode` or `bool`.""" |
|
263 return self.__cls |
|
264 |
|
265 @property |
|
266 def default(self): |
|
267 """The option's default value, may be `None`""" |
|
268 return self.__default |
|
269 |
|
270 @property |
|
271 def getter(self): |
|
272 """The getter method or function to get the option's value""" |
|
273 return self.__getter |
|
274 |
|
275 @property |
|
276 def validate(self): |
|
277 """A method or function to validate the value""" |
|
278 return self.__validate |
|
279 |
|
280 |
|
281 class Config(LazyConfig): |
|
282 """This class is for reading vmm's configuration file.""" |
|
283 |
|
284 def __init__(self, filename): |
|
285 """Creates a new Config instance |
|
286 |
|
287 Arguments: |
|
288 |
|
289 `filename` : str |
|
290 path to the configuration file |
|
291 """ |
|
292 LazyConfig.__init__(self) |
|
293 self._cfg_filename = filename |
|
294 self._cfg_file = None |
|
295 self.__missing = {} |
|
296 |
|
297 LCO = LazyConfigOption |
|
298 bool_t = self.bool_new |
|
299 self._cfg = { |
|
300 'account': { |
|
301 'delete_directory': LCO(bool_t, False, self.getboolean), |
|
302 'directory_mode': LCO(int, 448, self.getint), |
|
303 'disk_usage': LCO(bool_t, False, self.getboolean), |
|
304 'password_length': LCO(int, 8, self.getint), |
|
305 'random_password': LCO(bool_t, False, self.getboolean), |
|
306 'imap': LCO(bool_t, True, self.getboolean), |
|
307 'pop3': LCO(bool_t, True, self.getboolean), |
|
308 'sieve': LCO(bool_t, True, self.getboolean), |
|
309 'smtp': LCO(bool_t, True, self.getboolean), |
|
310 }, |
|
311 'bin': { |
|
312 'dovecotpw': LCO(str, '/usr/sbin/dovecotpw', self.get, |
|
313 exec_ok), |
|
314 'du': LCO(str, '/usr/bin/du', self.get, exec_ok), |
|
315 'postconf': LCO(str, '/usr/sbin/postconf', self.get, exec_ok), |
|
316 }, |
|
317 'database': { |
|
318 'host': LCO(str, 'localhost', self.get), |
|
319 'name': LCO(str, 'mailsys', self.get), |
|
320 'pass': LCO(str, None, self.get), |
|
321 'user': LCO(str, None, self.get), |
|
322 }, |
|
323 'domain': { |
|
324 'auto_postmaster': LCO(bool_t, True, self.getboolean), |
|
325 'delete_directory': LCO(bool_t, False, self.getboolean), |
|
326 'directory_mode': LCO(int, 504, self.getint), |
|
327 'force_deletion': LCO(bool_t, False, self.getboolean), |
|
328 }, |
|
329 'mailbox': { |
|
330 'folders': LCO(str, 'Drafts:Sent:Templates:Trash', |
|
331 self.unicode), |
|
332 'format': LCO(str, 'maildir', self.get, check_mailbox_format), |
|
333 'root': LCO(str, 'Maildir', self.unicode), |
|
334 'subscribe': LCO(bool_t, True, self.getboolean), |
|
335 }, |
|
336 'misc': { |
|
337 'base_directory': LCO(str, '/srv/mail', self.get, is_dir), |
|
338 'crypt_blowfish_rounds': LCO(int, 5, self.getint), |
|
339 'crypt_sha256_rounds': LCO(int, 5000, self.getint), |
|
340 'crypt_sha512_rounds': LCO(int, 5000, self.getint), |
|
341 'dovecot_version': LCO(str, None, self.hexversion, |
|
342 check_version_format), |
|
343 'password_scheme': LCO(str, 'CRAM-MD5', self.get, |
|
344 verify_scheme), |
|
345 'transport': LCO(str, 'dovecot:', self.get), |
|
346 }, |
|
347 } |
|
348 |
|
349 def load(self): |
|
350 """Loads the configuration, read only. |
|
351 |
|
352 Raises a ConfigError if the configuration syntax is |
|
353 invalid. |
|
354 """ |
|
355 try: |
|
356 self._cfg_file = open(self._cfg_filename, 'r') |
|
357 self.readfp(self._cfg_file) |
|
358 except (MissingSectionHeaderError, ParsingError), err: |
|
359 raise ConfigError(str(err), CONF_ERROR) |
|
360 finally: |
|
361 if self._cfg_file and not self._cfg_file.closed: |
|
362 self._cfg_file.close() |
|
363 |
|
364 def check(self): |
|
365 """Performs a configuration check. |
|
366 |
|
367 Raises a ConfigError if settings w/o a default value are missed. |
|
368 Or a ConfigValueError if 'misc.dovecot_version' has the wrong |
|
369 format. |
|
370 """ |
|
371 # TODO: There are only two settings w/o defaults. |
|
372 # So there is no need for cStringIO |
|
373 if not self.__chk_cfg(): |
|
374 errmsg = StringIO() |
|
375 errmsg.write(_(u'Missing options, which have no default value.\n')) |
|
376 errmsg.write(_(u'Using configuration file: %s\n') % |
|
377 self._cfg_filename) |
|
378 for section, options in self.__missing.iteritems(): |
|
379 errmsg.write(_(u'* Section: %s\n') % section) |
|
380 for option in options: |
|
381 errmsg.write((u' %s\n') % option) |
|
382 raise ConfigError(errmsg.getvalue(), CONF_ERROR) |
|
383 check_version_format(self.get('misc', 'dovecot_version')) |
|
384 |
|
385 def hexversion(self, section, option): |
|
386 """Converts the version number (e.g.: 1.2.3) from the *option*'s |
|
387 value to an int.""" |
|
388 return version_hex(self.get(section, option)) |
|
389 |
|
390 def unicode(self, section, option): |
|
391 """Returns the value of the `option` from `section`, converted |
|
392 to Unicode.""" |
|
393 return get_unicode(self.get(section, option)) |
|
394 |
|
395 def __chk_cfg(self): |
|
396 """Checks all section's options for settings w/o a default |
|
397 value. |
|
398 |
|
399 Returns `True` if everything is fine, else `False`. |
|
400 """ |
|
401 errors = False |
|
402 for section in self._cfg.iterkeys(): |
|
403 missing = [] |
|
404 for option, value in self._cfg[section].iteritems(): |
|
405 if (value.default is None and |
|
406 not RawConfigParser.has_option(self, section, option)): |
|
407 missing.append(option) |
|
408 errors = True |
|
409 if missing: |
|
410 self.__missing[section] = missing |
|
411 return not errors |
|
412 |
|
413 |
|
414 def check_mailbox_format(format): |
|
415 """ |
|
416 Check if the mailbox format *format* is supported. When the *format* |
|
417 is supported it will be returned, otherwise a `ConfigValueError` will |
|
418 be raised. |
|
419 """ |
|
420 format = format.lower() |
|
421 if known_format(format): |
|
422 return format |
|
423 raise ConfigValueError(_(u"Unsupported mailbox format: '%s'") % |
|
424 get_unicode(format)) |
|
425 |
|
426 |
|
427 def check_version_format(version_string): |
|
428 """Check if the *version_string* has the proper format, e.g.: '1.2.3'. |
|
429 Returns the validated version string if it has the expected format. |
|
430 Otherwise a `ConfigValueError` will be raised. |
|
431 """ |
|
432 version_re = r'^\d+\.\d+\.(?:\d+|(?:alpha|beta|rc)\d+)$' |
|
433 if not re.match(version_re, version_string): |
|
434 raise ConfigValueError(_(u"Not a valid Dovecot version: '%s'") % |
|
435 get_unicode(version_string)) |
|
436 return version_string |
|
437 |
|
438 |
|
439 def verify_scheme(scheme): |
|
440 """Checks if the password scheme *scheme* can be accepted and returns |
|
441 the verified scheme. |
|
442 """ |
|
443 try: |
|
444 scheme, encoding = _verify_scheme(scheme) |
|
445 except VMMError, err: # 'cast' it |
|
446 raise ConfigValueError(err.msg) |
|
447 if not encoding: |
|
448 return scheme |
|
449 return '%s.%s' % (scheme, encoding) |
|
450 |
|
451 del _ |