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)
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
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."""
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
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.
56 # recipient: your.address@your.domain"""
60 @implementer(IConfiguration
)
62 """The core global configuration object."""
65 self
.switchboards
= {}
66 self
.QFILE_SCHEMA_VERSION
= version
.QFILE_SCHEMA_VERSION
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.
78 self
.password_context
= None
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
)
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
)
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."""
111 self
._config
.push(config_name
, config_string
)
114 def pop(self
, config_name
):
115 """Pop a configuration from the stack."""
117 self
._config
.pop(config_name
)
120 def _post_process(self
):
121 """Perform post-processing after loading the configuration files."""
122 # Expand and set up all directories.
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
:
139 print('No path configuration found:', layout
, file=sys
.stderr
)
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
148 var_dir
= os
.environ
.get('MAILMAN_VAR_DIR', category
.var_dir
)
149 substitutions
= dict(
151 argv
=default_bin_dir
,
154 os
.path
.dirname(mailman
.templates
.__file
__)
155 if category
.template_dir
== ':source:'
156 else category
.template_dir
),
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
)
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
173 last_dollar_count
= 0
176 # Mutate the dictionary during iteration.
177 for key
in substitutions
:
178 raw_value
= substitutions
[key
]
179 value
= Template(raw_value
).safe_substitute(substitutions
)
181 # Still more work to do.
182 expandables
.append((key
, value
))
183 substitutions
[key
] = value
184 if len(expandables
) == 0:
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
)),
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
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
]))
202 def logger_configs(self
):
203 """Return all log config sections."""
204 return self
._config
.getByCategory('logging', [])
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():
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
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
)
233 def runner_configs(self
):
234 """Iterate over all the runner configuration sections."""
235 yield from self
._config
.getByCategory('runner', [])
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:
244 archiver
= call_name(class_path
)
245 archiver
.is_enabled
= as_boolean(section
.enable
)
249 def language_configs(self
):
250 """Iterate over all the language configuration sections."""
251 yield from self
._config
.getByCategory('language', [])
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
264 :return: The contents of the configuration file.
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
:
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
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
)