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