Clean up the mta directory.
[mailman.git] / src / mailman / config / config.py
blob415e1e3d22a8ce2332511acd4e2461a51b0e6a6b
1 # Copyright (C) 2006-2016 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Configuration file loading and management."""
20 import os
21 import sys
22 import mailman.templates
24 from configparser import ConfigParser
25 from flufl.lock import Lock
26 from lazr.config import ConfigSchema, as_boolean
27 from mailman import public, version
28 from mailman.interfaces.configuration import (
29 ConfigurationUpdatedEvent, IConfiguration, MissingConfigurationFileError)
30 from mailman.interfaces.languages import ILanguageManager
31 from mailman.utilities.filesystem import makedirs
32 from mailman.utilities.modules import call_name, expand_path
33 from pkg_resources import resource_filename, resource_string as resource_bytes
34 from string import Template
35 from zope.component import getUtility
36 from zope.event import notify
37 from zope.interface import implementer
40 SPACE = ' '
41 SPACERS = '\n'
43 MAILMAN_CFG_TEMPLATE = """\
44 # AUTOMATICALLY GENERATED BY MAILMAN ON {}
46 # This is your GNU Mailman 3 configuration file. You can edit this file to
47 # configure Mailman to your needs, and Mailman will never overwrite it.
48 # Additional configuration information is (for now) available in the
49 # schema.cfg file <http://tinyurl.com/cm5rtqe> and the base mailman.cfg file
50 # <http://tinyurl.com/dx9b8eg>.
52 # For example, uncomment the following lines to run Mailman in developer mode.
54 # [devmode]
55 # enabled: yes
56 # recipient: your.address@your.domain"""
59 @public
60 @implementer(IConfiguration)
61 class Configuration:
62 """The core global configuration object."""
64 def __init__(self):
65 self.switchboards = {}
66 self.QFILE_SCHEMA_VERSION = version.QFILE_SCHEMA_VERSION
67 self._config = None
68 self.filename = None
69 # Whether to create run-time paths or not. This is for the test
70 # suite, which will set this to False until the test layer is set up.
71 self.create_paths = True
72 # Create various registries.
73 self.chains = {}
74 self.rules = {}
75 self.handlers = {}
76 self.pipelines = {}
77 self.commands = {}
78 self.password_context = None
80 def _clear(self):
81 """Clear the cached configuration variables."""
82 self.switchboards.clear()
83 getUtility(ILanguageManager).clear()
85 def __getattr__(self, name):
86 """Delegate to the configuration object."""
87 return getattr(self._config, name)
89 def __iter__(self):
90 return iter(self._config)
92 def load(self, filename=None):
93 """Load the configuration from the schema and config files."""
94 schema_file = resource_filename('mailman.config', 'schema.cfg')
95 schema = ConfigSchema(schema_file)
96 # If a configuration file was given, load it now too. First, load
97 # the absolute minimum default configuration, then if a
98 # configuration filename was given by the user, push it.
99 config_file = resource_filename('mailman.config', 'mailman.cfg')
100 self._config = schema.load(config_file)
101 if filename is None:
102 self._post_process()
103 else:
104 self.filename = filename
105 with open(filename, 'r', encoding='utf-8') as user_config:
106 self.push(filename, user_config.read())
108 def push(self, config_name, config_string):
109 """Push a new configuration onto the stack."""
110 self._clear()
111 self._config.push(config_name, config_string)
112 self._post_process()
114 def pop(self, config_name):
115 """Pop a configuration from the stack."""
116 self._clear()
117 self._config.pop(config_name)
118 self._post_process()
120 def _post_process(self):
121 """Perform post-processing after loading the configuration files."""
122 # Expand and set up all directories.
123 self._expand_paths()
124 self.ensure_directories_exist()
125 notify(ConfigurationUpdatedEvent(self))
127 def _expand_paths(self):
128 """Expand all configuration paths."""
129 # Set up directories.
130 default_bin_dir = os.path.abspath(os.path.dirname(sys.executable))
131 # Now that we've loaded all the configuration files we're going to
132 # load, set up some useful directories based on the settings in the
133 # configuration file.
134 layout = 'paths.' + self._config.mailman.layout
135 for category in self._config.getByCategory('paths'):
136 if category.name == layout:
137 break
138 else:
139 print('No path configuration found:', layout, file=sys.stderr)
140 sys.exit(1)
141 # First, collect all variables in a substitution dictionary. $VAR_DIR
142 # is taken from the environment or from the configuration file if the
143 # environment is not set. Because the var_dir setting in the config
144 # file could be a relative path, and because 'mailman start' chdirs to
145 # $VAR_DIR, without this subprocesses bin/master and bin/runner will
146 # create $VAR_DIR hierarchies under $VAR_DIR when that path is
147 # relative.
148 var_dir = os.environ.get('MAILMAN_VAR_DIR', category.var_dir)
149 substitutions = dict(
150 cwd=os.getcwd(),
151 argv=default_bin_dir,
152 var_dir=var_dir,
153 template_dir=(
154 os.path.dirname(mailman.templates.__file__)
155 if category.template_dir == ':source:'
156 else category.template_dir),
158 # Directories.
159 for name in ('archive', 'bin', 'data', 'etc', 'ext', 'list_data',
160 'lock', 'log', 'messages', 'queue'):
161 key = '{}_dir'.format(name)
162 substitutions[key] = getattr(category, key)
163 # Files.
164 for name in ('lock', 'pid'):
165 key = '{}_file'.format(name)
166 substitutions[key] = getattr(category, key)
167 # Add the path to the .cfg file, if one was given on the command line.
168 if self.filename is not None:
169 substitutions['cfg_file'] = self.filename
170 # Now, perform substitutions recursively until there are no more
171 # variables with $-vars in them, or until substitutions are not
172 # helping any more.
173 last_dollar_count = 0
174 while True:
175 expandables = []
176 # Mutate the dictionary during iteration.
177 for key in substitutions:
178 raw_value = substitutions[key]
179 value = Template(raw_value).safe_substitute(substitutions)
180 if '$' in value:
181 # Still more work to do.
182 expandables.append((key, value))
183 substitutions[key] = value
184 if len(expandables) == 0:
185 break
186 if len(expandables) == last_dollar_count:
187 print('Path expansion infloop detected:\n',
188 SPACERS.join('\t{}: {}'.format(key, value)
189 for key, value in sorted(expandables)),
190 file=sys.stderr)
191 sys.exit(1)
192 last_dollar_count = len(expandables)
193 # Ensure that all paths are normalized and made absolute. Handle the
194 # few special cases first. Most of these are due to backward
195 # compatibility.
196 self.PID_FILE = os.path.abspath(substitutions.pop('pid_file'))
197 for key in substitutions:
198 attribute = key.upper()
199 setattr(self, attribute, os.path.abspath(substitutions[key]))
201 @property
202 def logger_configs(self):
203 """Return all log config sections."""
204 return self._config.getByCategory('logging', [])
206 @property
207 def paths(self):
208 """Return a substitution dictionary of all path variables."""
209 return dict((k, self.__dict__[k])
210 for k in self.__dict__
211 if k.endswith('_DIR'))
213 def ensure_directories_exist(self):
214 """Create all path directories if they do not exist."""
215 if self.create_paths:
216 for variable, directory in self.paths.items():
217 makedirs(directory)
218 # Avoid circular imports.
219 from mailman.utilities.datetime import now
220 # Create a mailman.cfg template file if it doesn't already exist.
221 # LBYL: <boo hiss>, but it's probably okay because the directories
222 # likely didn't exist before the above loop, and we'll create a
223 # temporary lock.
224 lock_file = os.path.join(self.LOCK_DIR, 'mailman-cfg.lck')
225 mailman_cfg = os.path.join(self.ETC_DIR, 'mailman.cfg')
226 with Lock(lock_file):
227 if not os.path.exists(mailman_cfg):
228 with open(mailman_cfg, 'w') as fp:
229 print(MAILMAN_CFG_TEMPLATE.format(
230 now().replace(microsecond=0)), file=fp)
232 @property
233 def runner_configs(self):
234 """Iterate over all the runner configuration sections."""
235 yield from self._config.getByCategory('runner', [])
237 @property
238 def archivers(self):
239 """Iterate over all the archivers."""
240 for section in self._config.getByCategory('archiver', []):
241 class_path = section['class'].strip()
242 if len(class_path) == 0:
243 continue
244 archiver = call_name(class_path)
245 archiver.is_enabled = as_boolean(section.enable)
246 yield archiver
248 @property
249 def language_configs(self):
250 """Iterate over all the language configuration sections."""
251 yield from self._config.getByCategory('language', [])
254 @public
255 def load_external(path):
256 """Load the configuration file named by path.
258 :param path: A string naming the location of the external configuration
259 file. This is either an absolute file system path or a special
260 ``python:`` path. When path begins with ``python:``, the rest of the
261 value must name a ``.cfg`` file located within Python's import path,
262 however the trailing ``.cfg`` suffix is implied (don't provide it
263 here).
264 :return: The contents of the configuration file.
265 :rtype: str
267 # Is the context coming from a file system or Python path?
268 if path.startswith('python:'):
269 resource_path = path[7:]
270 package, dot, resource = resource_path.rpartition('.')
271 return resource_bytes(package, resource + '.cfg').decode('utf-8')
272 with open(path, 'r', encoding='utf-8') as fp:
273 return fp.read()
276 @public
277 def external_configuration(path):
278 """Parse the configuration file named by path.
280 :param path: A string naming the location of the external configuration
281 file. This is either an absolute file system path or a special
282 ``python:`` path. When path begins with ``python:``, the rest of the
283 value must name a ``.cfg`` file located within Python's import path,
284 however the trailing ``.cfg`` suffix is implied (don't provide it
285 here).
286 :return: A `ConfigParser` instance.
288 # Is the context coming from a file system or Python path?
289 cfg_path = expand_path(path)
290 parser = ConfigParser()
291 files = parser.read(cfg_path)
292 if files != [cfg_path]:
293 raise MissingConfigurationFileError(path)
294 return parser