def remove_list(fqdn_listname, mailing_list=None): """Remove the list and all associated artifacts and subscriptions.""" removeables = [] # mailing_list will be None when only residual archives are being removed. if mailing_list is not None: # Remove all subscriptions, regardless of role. for member in mailing_list.subscribers.members: member.unsubscribe() # Delete the mailing list from the database. getUtility(IListManager).delete(mailing_list) # Do the MTA-specific list deletion tasks call_name(config.mta.incoming).create(mailing_list) # Remove the list directory. removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname)) # Remove any stale locks associated with the list. for filename in os.listdir(config.LOCK_DIR): fn_listname = filename.split('.')[0] if fn_listname == fqdn_listname: removeables.append(os.path.join(config.LOCK_DIR, filename)) # Now that we know what files and directories to delete, delete them. for target in removeables: if not os.path.exists(target): pass elif os.path.islink(target): os.unlink(target) elif os.path.isdir(target): shutil.rmtree(target) elif os.path.isfile(target): os.unlink(target) else: log.error('Could not delete list artifact: %s', target)
def remove_list(mlist): """Remove the list and all associated artifacts and subscriptions.""" fqdn_listname = mlist.fqdn_listname removeables = [] # Delete the mailing list from the database. getUtility(IListManager).delete(mlist) # Do the MTA-specific list deletion tasks call_name(config.mta.incoming).delete(mlist) # Remove the list directory. removeables.append(os.path.join(config.LIST_DATA_DIR, fqdn_listname)) # Remove any stale locks associated with the list. for filename in os.listdir(config.LOCK_DIR): fn_listname, dot, rest = filename.partition('.') if fn_listname == fqdn_listname: removeables.append(os.path.join(config.LOCK_DIR, filename)) # Now that we know what files and directories to delete, delete them. for target in removeables: if not os.path.exists(target): pass elif os.path.islink(target): os.unlink(target) elif os.path.isdir(target): shutil.rmtree(target) elif os.path.isfile(target): os.unlink(target) else: log.error('Could not delete list artifact: %s', target)
def initialize_3(): """Third initialization step. * Post-hook """ # Run the post-hook if there is one. config = mailman.config.config if config.mailman.post_hook: call_name(config.mailman.post_hook)
def remove_list(mlist): """Remove the list and all associated artifacts and subscriptions.""" # Remove the list's data directory, if it exists. with suppress(FileNotFoundError): shutil.rmtree(mlist.data_path) # Delete the mailing list from the database. getUtility(IListManager).delete(mlist) # Do the MTA-specific list deletion tasks call_name(config.mta.incoming).delete(mlist)
def remove_list(mlist): """Remove the list and all associated artifacts and subscriptions.""" fqdn_listname = mlist.fqdn_listname # Delete the mailing list from the database. getUtility(IListManager).delete(mlist) # Do the MTA-specific list deletion tasks call_name(config.mta.incoming).delete(mlist) # Remove the list directory, if it exists. try: shutil.rmtree(os.path.join(config.LIST_DATA_DIR, fqdn_listname)) except OSError as error: if error.errno != errno.ENOENT: raise
def start(ctx, force, generate_alias_file, run_as_user, quiet): # Although there's a potential race condition here, it's a better user # experience for the parent process to refuse to start twice, rather than # having it try to start the master, which will error exit. status, lock = master_state() if status is WatcherState.conflict: ctx.fail(_('GNU Mailman is already running')) elif status in (WatcherState.stale_lock, WatcherState.host_mismatch): if not force: ctx.fail( _('A previous run of GNU Mailman did not exit ' 'cleanly ({}). Try using --force'.format(status.name))) # Daemon process startup according to Stevens, Advanced Programming in the # UNIX Environment, Chapter 13. pid = os.fork() if pid: # parent if not quiet: print(_("Starting Mailman's master runner")) if generate_alias_file: if not quiet: print(_("Generating MTA alias maps")) call_name(config.mta.incoming).regenerate() return # child: Create a new session and become the session leader, but since we # won't be opening any terminal devices, don't do the ultra-paranoid # suggestion of doing a second fork after the setsid() call. os.setsid() # Instead of cd'ing to root, cd to the Mailman runtime directory. However, # before we do that, set an environment variable used by the subprocesses # to calculate their path to the $VAR_DIR. os.environ['MAILMAN_VAR_DIR'] = config.VAR_DIR os.chdir(config.VAR_DIR) # Exec the master watcher. execl_args = [ sys.executable, sys.executable, os.path.join(config.BIN_DIR, 'master'), ] if force: execl_args.append('--force') # Always pass the configuration file path to the master process, so there's # no confusion about which one is being used. execl_args.extend(['-C', config.filename]) qlog.debug('starting: %s', execl_args) os.execl(*execl_args) # We should never get here. raise RuntimeError('os.execl() failed')
def archivers(self): """Iterate over all the enabled archivers.""" for section in self._config.getByCategory("archiver", []): if not as_boolean(section.enable): continue class_path = section["class"] yield call_name(class_path)
def confirm(self, store, token, expunge=True): # Token can come in as a unicode, but it's stored in the database as # bytes. They must be ascii. pendings = store.query(Pended).filter_by(token=str(token)) if pendings.count() == 0: return None assert pendings.count() == 1, ( 'Unexpected token count: {0}'.format(pendings.count())) pending = pendings[0] pendable = UnpendedPendable() # Find all PendedKeyValue entries that are associated with the pending # object's ID. Watch out for type conversions. entries = store.query(PendedKeyValue).filter( PendedKeyValue.pended_id == pending.id) for keyvalue in entries: if keyvalue.value is not None and '\1' in keyvalue.value: type_name, value = keyvalue.value.split('\1', 1) pendable[keyvalue.key] = call_name(type_name, value) else: pendable[keyvalue.key] = keyvalue.value if expunge: store.delete(keyvalue) if expunge: store.delete(pending) return pendable
def create(): """See `IDatabaseFactory`.""" database_class_name = config.database['class'] database = call_name(database_class_name) verifyObject(IDatabase, database) adapted_database = getAdapter( database, ITemporaryDatabase, database.TAG) return adapted_database
def create_list(fqdn_listname, owners=None, style_name=None): """Create the named list and apply styles. The mailing may not exist yet, but the domain specified in `fqdn_listname` must exist. :param fqdn_listname: The fully qualified name for the new mailing list. :type fqdn_listname: string :param owners: The mailing list owners. :type owners: list of string email addresses :param style_name: The name of the style to apply to the newly created list. If not given, the default is taken from the configuration file. :type style_name: string :return: The new mailing list. :rtype: `IMailingList` :raises BadDomainSpecificationError: when the hostname part of `fqdn_listname` does not exist. :raises ListAlreadyExistsError: when the mailing list already exists. :raises InvalidEmailAddressError: when the fqdn email address is invalid. """ if owners is None: owners = [] # This raises InvalidEmailAddressError if the address is not a valid # posting address. Let these percolate up. getUtility(IEmailValidator).validate(fqdn_listname) listname, domain = fqdn_listname.split('@', 1) if domain not in getUtility(IDomainManager): raise BadDomainSpecificationError(domain) mlist = getUtility(IListManager).create(fqdn_listname) style = getUtility(IStyleManager).get( config.styles.default if style_name is None else style_name) if style is not None: style.apply(mlist) # Coordinate with the MTA, as defined in the configuration file. call_name(config.mta.incoming).create(mlist) # Create any owners that don't yet exist, and subscribe all addresses as # owners of the mailing list. user_manager = getUtility(IUserManager) for owner_address in owners: address = user_manager.get_address(owner_address) if address is None: user = user_manager.create_user(owner_address) address = list(user.addresses)[0] mlist.subscribe(address, MemberRole.owner) return mlist
def archivers(self): """Iterate over all the archivers.""" for section in self._config.getByCategory('archiver', []): class_path = section['class'].strip() if len(class_path) == 0: continue archiver = call_name(class_path) archiver.is_enabled = as_boolean(section.enable) yield archiver
def create(): """See `IDatabaseFactory`.""" database_class = config.database['class'] database = call_name(database_class) verifyObject(IDatabase, database) database.initialize() database.load_migrations() database.commit() return database
def create(): """See `IDatabaseFactory`.""" with Lock(os.path.join(config.LOCK_DIR, 'dbcreate.lck')): database_class = config.database['class'] database = call_name(database_class) verifyObject(IDatabase, database) database.initialize() SchemaManager(database).setup_database() database.commit() return database
def process(self, args): """See `ICLISubCommand`.""" if args.format is not None and args.simple: self.parser.error(_('Cannot use both -s and -f')) # Does not return. output = None if args.output == '-': output = sys.stdout elif args.output is None: output = None else: output = args.output if args.simple: Dummy().regenerate(output) else: format_arg = (config.mta.incoming if args.format is None else args.format) # Call the MTA-specific regeneration method. call_name(format_arg).regenerate(output)
def create(): """See `IDatabaseFactory`.""" database_class = config.database['class'] database = call_name(database_class) verifyObject(IDatabase, database) database.initialize() Model.metadata.create_all(database.engine) database.commit() # Make _reset() a bound method of the database instance. database._reset = types.MethodType(_reset, database) return database
def create(): """See `IDatabaseFactory`.""" database_class = config.database['class'] database = call_name(database_class) verifyObject(IDatabase, database) database.initialize() database.load_migrations() database.commit() # Make _reset() a bound method of the database instance. database._reset = types.MethodType(_reset, database) return database
def initialize_2(debug=False, propagate_logs=None): """Second initialization step. * Logging * Pre-hook * Rules * Chains * Pipelines * Commands :param debug: Should the database layer be put in debug mode? :type debug: boolean :param propagate_logs: Should the log output propagate to stderr? :type propagate_logs: boolean or None """ # Create the queue and log directories if they don't already exist. mailman.core.logging.initialize(propagate_logs) # Run the pre-hook if there is one. config = mailman.config.config if config.mailman.pre_hook: call_name(config.mailman.pre_hook) # Instantiate the database class, ensure that it's of the right type, and # initialize it. Then stash the object on our configuration object. database_class = config.database['class'] database = call_name(database_class) verifyObject(IDatabase, database) database.initialize(debug) config.db = database # Initialize the rules and chains. Do the imports here so as to avoid # circular imports. from mailman.app.commands import initialize as initialize_commands from mailman.app.events import initialize as initialize_events from mailman.core.chains import initialize as initialize_chains from mailman.core.pipelines import initialize as initialize_pipelines from mailman.core.rules import initialize as initialize_rules # Order here is somewhat important. initialize_rules() initialize_chains() initialize_pipelines() initialize_commands() initialize_events()
def populate(self): self._styles.clear() # Avoid circular imports. from mailman.config import config # Install all the styles described by the configuration files. for section in config.style_configs: class_path = section['class'] style = call_name(class_path) assert section.name.startswith('style'), ( 'Bad style section name: %s' % section.name) style.name = section.name[6:] style.priority = int(section.priority) self.register(style)
def shell(ctx, interactive, run, listspec, run_args): global m, r banner = DEFAULT_BANNER # Interactive is the default unless --run was given. interactive = (run is None) if interactive is None else interactive # List name cannot be a regular expression if --run is not given. if listspec and listspec.startswith('^') and not run: ctx.fail(_('Regular expression requires --run')) # Handle --run. list_manager = getUtility(IListManager) if run: # When the module and the callable have the same name, a shorthand # without the dot is allowed. dotted_name = (run if '.' in run else '{0}.{0}'.format(run)) if listspec is None: r = call_name(dotted_name, *run_args) elif listspec.startswith('^'): r = {} cre = re.compile(listspec, re.IGNORECASE) for mlist in list_manager.mailing_lists: if cre.match(mlist.fqdn_listname) or cre.match(mlist.list_id): results = call_name(dotted_name, mlist, *run_args) r[mlist.list_id] = results else: m = list_manager.get(listspec) if m is None: ctx.fail(_('No such list: $listspec')) r = call_name(dotted_name, m, *run_args) else: # Not --run. if listspec is not None: m = list_manager.get(listspec) if m is None: ctx.fail(_('No such list: $listspec')) banner = _("The variable 'm' is the $listspec mailing list") # All other processing is finished; maybe go into interactive mode. if interactive: do_interactive(ctx, banner)
def initialize_2(debug=False, propagate_logs=None, testing=False): """Second initialization step. * Database * Logging * Pre-hook * Rules * Chains * Pipelines * Commands :param debug: Should the database layer be put in debug mode? :type debug: boolean :param propagate_logs: Should the log output propagate to stderr? :type propagate_logs: boolean or None """ # Create the queue and log directories if they don't already exist. mailman.core.logging.initialize(propagate_logs) # Run the pre-hook if there is one. config = mailman.config.config if config.mailman.pre_hook: call_name(config.mailman.pre_hook) # Instantiate the database class, ensure that it's of the right type, and # initialize it. Then stash the object on our configuration object. utility_name = ('testing' if testing else 'production') config.db = getUtility(IDatabaseFactory, utility_name).create() # Initialize the rules and chains. Do the imports here so as to avoid # circular imports. from mailman.app.commands import initialize as initialize_commands from mailman.core.chains import initialize as initialize_chains from mailman.core.pipelines import initialize as initialize_pipelines from mailman.core.rules import initialize as initialize_rules # Order here is somewhat important. initialize_rules() initialize_chains() initialize_pipelines() initialize_commands()
def create(): """See `IDatabaseFactory`.""" database_class = config.database['class'] database = call_name(database_class) verifyObject(IDatabase, database) database.initialize() # Remove existing tables (PostgreSQL will keep them across runs) metadata = MetaData(bind=database.engine) metadata.reflect() metadata.drop_all() database.commit() # Now create the current model without Alembic upgrades. Model.metadata.create_all(database.engine) database.commit() # Make _reset() a bound method of the database instance. database._reset = types.MethodType(_reset, database) return database
def initialize(): """Initialize all enabled plugins.""" for name, plugin_config in config.plugin_configs: class_path = plugin_config['class'].strip() if not as_boolean(plugin_config['enabled']) or len(class_path) == 0: log.info( 'Plugin not enabled, or empty class path: {}'.format(name)) continue if name in config.plugins: log.error('Duplicate plugin name: {}'.format(name)) continue plugin = call_name(class_path) try: verifyObject(IPlugin, plugin) except DoesNotImplement: log.error('Plugin class does not implement IPlugin: {}'.format( class_path)) continue plugin.name = name config.plugins[name] = plugin
def process(self, args): """See `ICLISubCommand`.""" # Call the MTA-specific regeneration method. call_name(config.mta.incoming).regenerate(args.directory)
def process(self, args): """See `ICLISubCommand`.""" global m, r banner = DEFAULT_BANNER # Detailed help wanted? if args.details: self._details() return # Interactive is the default unless --run was given. if args.interactive is None: interactive = (args.run is None) else: interactive = args.interactive # List name cannot be a regular expression if --run is not given. if args.listname and args.listname.startswith('^') and not args.run: self.parser.error(_('Regular expression requires --run')) return # Handle --run. list_manager = getUtility(IListManager) if args.run: # When the module and the callable have the same name, a shorthand # without the dot is allowed. dotted_name = (args.run if '.' in args.run else '{0}.{0}'.format(args.run)) if args.listname is None: self.parser.error(_('--run requires a mailing list name')) return elif args.listname.startswith('^'): r = {} cre = re.compile(args.listname, re.IGNORECASE) for mailing_list in list_manager.mailing_lists: if cre.match(mailing_list.fqdn_listname): results = call_name(dotted_name, mailing_list) r[mailing_list.fqdn_listname] = results else: fqdn_listname = args.listname m = list_manager.get(fqdn_listname) if m is None: self.parser.error(_('No such list: $fqdn_listname')) return r = call_name(dotted_name, m) else: # Not --run. if args.listname is not None: fqdn_listname = args.listname m = list_manager.get(fqdn_listname) if m is None: self.parser.error(_('No such list: $fqdn_listname')) return banner = _( "The variable 'm' is the $fqdn_listname mailing list") # All other processing is finished; maybe go into interactive mode. if interactive: overrides = dict(m=m, commit=config.db.commit, abort=config.db.abort, config=config, getUtility=getUtility) # Bootstrap some useful names into the namespace, mostly to make # the component architecture and interfaces easily available. for module_name in sys.modules: if not module_name.startswith('mailman.interfaces.'): continue module = sys.modules[module_name] for name in module.__all__: overrides[name] = getattr(module, name) banner = config.shell.banner + '\n' + (banner if isinstance( banner, str) else '') try: use_ipython = as_boolean(config.shell.use_ipython) except ValueError: if config.shell.use_ipython == 'debug': use_ipython = True debug = True else: raise else: debug = False if use_ipython: self._start_ipython(overrides, banner, debug) else: self._start_python(overrides, banner)
def create_list(fqdn_listname, owners=None, style_name=None): """Create the named list and apply styles. The mailing may not exist yet, but the domain specified in `fqdn_listname` must exist. :param fqdn_listname: The fully qualified name for the new mailing list. :type fqdn_listname: string :param owners: The mailing list owners. :type owners: list of string email addresses :param style_name: The name of the style to apply to the newly created list. If not given, the default is taken from the configuration file. :type style_name: string :return: The new mailing list. :rtype: `IMailingList` :raises BadDomainSpecificationError: when the hostname part of `fqdn_listname` does not exist. :raises ListAlreadyExistsError: when the mailing list already exists. :raises InvalidEmailAddressError: when the fqdn email address is invalid. :raises InvalidListNameError: when the fqdn email address is valid but the listname contains disallowed characters. """ if owners is None: owners = [] # This raises InvalidEmailAddressError if the address is not a valid # posting address. Let these percolate up. getUtility(IEmailValidator).validate(fqdn_listname) listname, domain = fqdn_listname.split('@', 1) # We need to be fussier than just validating the posting address. Various # legal local-part characters will cause problems in list names. # First we check our maximally allowed set. if len(_listname_chars.sub('', listname)) > 0: raise InvalidListNameError(listname) # Then if another set is configured, check that. if config.mailman.listname_chars: try: cre = re.compile(config.mailman.listname_chars, re.IGNORECASE) except re.error as error: log.error( 'Bad config.mailman.listname_chars setting: %s: %s', config.mailman.listname_chars, getattr(error, 'msg', str(error)) ) else: if len(cre.sub('', listname)) > 0: raise InvalidListNameError(listname) if domain not in getUtility(IDomainManager): raise BadDomainSpecificationError(domain) mlist = getUtility(IListManager).create(fqdn_listname) style = getUtility(IStyleManager).get( config.styles.default if style_name is None else style_name) if style is not None: style.apply(mlist) # Coordinate with the MTA, as defined in the configuration file. call_name(config.mta.incoming).create(mlist) # Create any owners that don't yet exist, and subscribe all addresses as # owners of the mailing list. user_manager = getUtility(IUserManager) for owner_address in owners: address = user_manager.get_address(owner_address) if address is None: user = user_manager.create_user(owner_address) address = list(user.addresses)[0] mlist.subscribe(address, MemberRole.owner) return mlist
def process(self, args): """See `ICLISubCommand`.""" global m, r banner = DEFAULT_BANNER # Detailed help wanted? if args.details: self._details() return # Interactive is the default unless --run was given. if args.interactive is None: interactive = (args.run is None) else: interactive = args.interactive # List name cannot be a regular expression if --run is not given. if args.listname and args.listname.startswith('^') and not args.run: self.parser.error(_('Regular expression requires --run')) return # Handle --run. list_manager = getUtility(IListManager) if args.run: # When the module and the callable have the same name, a shorthand # without the dot is allowed. dotted_name = (args.run if '.' in args.run else '{0}.{0}'.format(args.run)) if args.listname is None: self.parser.error(_('--run requires a mailing list name')) return elif args.listname.startswith('^'): r = {} cre = re.compile(args.listname, re.IGNORECASE) for mailing_list in list_manager.mailing_lists: if cre.match(mailing_list.fqdn_listname): results = call_name(dotted_name, mailing_list) r[mailing_list.fqdn_listname] = results else: fqdn_listname = args.listname m = list_manager.get(fqdn_listname) if m is None: self.parser.error(_('No such list: $fqdn_listname')) return r = call_name(dotted_name, m) else: # Not --run. if args.listname is not None: fqdn_listname = args.listname m = list_manager.get(fqdn_listname) if m is None: self.parser.error(_('No such list: $fqdn_listname')) return banner = _( "The variable 'm' is the $fqdn_listname mailing list") # All other processing is finished; maybe go into interactive mode. if interactive: overrides = dict( m=m, commit=config.db.commit, abort=config.db.abort, config=config, getUtility=getUtility ) # Bootstrap some useful names into the namespace, mostly to make # the component architecture and interfaces easily available. for module_name in sys.modules: if not module_name.startswith('mailman.interfaces.'): continue module = sys.modules[module_name] for name in module.__all__: overrides[name] = getattr(module, name) banner = config.shell.banner + '\n' + ( banner if isinstance(banner, str) else '') try: use_ipython = as_boolean(config.shell.use_ipython) except ValueError: if config.shell.use_ipython == 'debug': use_ipython = True debug = True else: raise else: debug = False if use_ipython: self._start_ipython(overrides, banner, debug) else: self._start_python(overrides, banner)
def aliases(directory): call_name(config.mta.incoming).regenerate(directory)
def process(self, args): """See `ICLISubCommand`.""" global m, r banner = DEFAULT_BANNER # Detailed help wanted? if args.details: self._details() return # Interactive is the default unless --run was given. if args.interactive is None: interactive = (args.run is None) else: interactive = args.interactive # List name cannot be a regular expression if --run is not given. if args.listname and args.listname.startswith('^') and not args.run: self.parser.error(_('Regular expression requires --run')) return # Handle --run. list_manager = getUtility(IListManager) if args.run: # When the module and the callable have the same name, a shorthand # without the dot is allowed. dotted_name = (args.run if '.' in args.run else '{0}.{0}'.format(args.run)) if args.listname is None: self.parser.error(_('--run requires a mailing list name')) return elif args.listname.startswith('^'): r = {} cre = re.compile(args.listname, re.IGNORECASE) for mailing_list in list_manager.mailing_lists: if cre.match(mailing_list.fqdn_listname): results = call_name(dotted_name, mailing_list) r[mailing_list.fqdn_listname] = results else: fqdn_listname = args.listname m = list_manager.get(fqdn_listname) if m is None: self.parser.error(_('No such list: $fqdn_listname')) return r = call_name(dotted_name, m) else: # Not --run. if args.listname is not None: fqdn_listname = args.listname m = list_manager.get(fqdn_listname) if m is None: self.parser.error(_('No such list: $fqdn_listname')) return banner = _( "The variable 'm' is the $fqdn_listname mailing list") # All other processing is finished; maybe go into interactive mode. if interactive: overrides = dict( m=m, commit=config.db.commit, abort=config.db.abort, config=config, ) banner = config.shell.banner + '\n' + banner if as_boolean(config.shell.use_ipython): self._start_ipython(overrides, banner) else: self._start_python(overrides, banner)