def __init__(self): """Create a basic deliverer.""" self._connection = Connection( config.mta.smtp_host, int(config.mta.smtp_port), int(config.mta.max_sessions_per_connection), config.mta.smtp_user if config.mta.smtp_user else None, config.mta.smtp_pass if config.mta.smtp_pass else None, as_SecureMode(config.mta.smtp_secure_mode), as_boolean(config.mta.smtp_verify_cert), as_boolean(config.mta.smtp_verify_hostname), )
def do_interactive(ctx, banner): global m, r 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: print(_('Invalid value for [shell]use_python: {}').format( config.shell.use_ipython), file=sys.stderr) return else: debug = False if use_ipython: start_ipython(overrides, banner, debug) else: start_python(overrides, banner)
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = _get_message(mlist.welcome_message_uri, mlist, language) options_url = member.options_url # Get the text from the template. display_name = ('' if member.user is None else member.user.display_name) text = expand(welcome_message, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=display_name, user_address=member.address.email, user_options_uri=options_url, )) digmode = ('' if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr((display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def etag(resource): """Calculate the etag and return a JSON representation. The input is a dictionary representing the resource. This dictionary must not contain an `http_etag` key. This function calculates the etag by using the sha1 hexdigest of the pretty-printed (and thus key-sorted and predictable) representation of the dictionary. It then inserts this value under the `http_etag` key, and returns the JSON representation of the modified dictionary. :param resource: The original resource representation. :type resource: dictionary :return: JSON representation of the modified dictionary. :rtype string """ assert 'http_etag' not in resource, 'Resource already etagged' # Calculate the tag from a predictable (i.e. sorted) representation of the # dictionary. The actual details aren't so important. pformat() is # guaranteed to sort the keys, however it returns a str and the hash # library requires a bytes. Use the safest possible encoding. hashfood = pformat(resource).encode('raw-unicode-escape') etag = hashlib.sha1(hashfood).hexdigest() resource['http_etag'] = '"{}"'.format(etag) return json.dumps(resource, cls=ExtendedEncoder, sort_keys=as_boolean(config.devmode.enabled))
def info(output, verbose): """See `ICLISubCommand`.""" print(MAILMAN_VERSION_FULL, file=output) print('Python', sys.version, file=output) print('config file:', config.filename, file=output) print('db url:', config.db.url, file=output) print('devmode:', 'ENABLED' if as_boolean(config.devmode.enabled) else 'DISABLED', file=output) api = (API30 if config.webservice.api_version == '3.0' else API31) print('REST root url:', api.path_to('/'), file=output) print('REST credentials: {}:{}'.format(config.webservice.admin_user, config.webservice.admin_pass), file=output) if verbose: print('File system paths:', file=output) longest = 0 paths = {} for attribute in dir(config): if attribute.endswith('_DIR') or attribute.endswith('_FILE'): paths[attribute] = getattr(config, attribute) longest = max(longest, len(attribute)) for attribute in sorted(paths): print(' {0:{2}} = {1}'.format(attribute, paths[attribute], longest))
def __init__(self, name, slice=None): """Create a runner. :param slice: The slice number for this runner. This is passed directly to the underlying `ISwitchboard` object. This is ignored for runners that don't manage a queue. :type slice: int or None """ # Grab the configuration section. self.name = name section = getattr(config, 'runner.' + name) substitutions = config.paths substitutions['name'] = name numslices = int(section.instances) # Check whether the runner is queue runner or not; non-queue runner # should not have queue_directory or switchboard instance. if self.is_queue_runner: self.queue_directory = expand(section.path, substitutions) self.switchboard = Switchboard( name, self.queue_directory, slice, numslices, True) else: self.queue_directory = None self.switchboard= None self.sleep_time = as_timedelta(section.sleep_time) # sleep_time is a timedelta; turn it into a float for time.sleep(). self.sleep_float = (86400 * self.sleep_time.days + self.sleep_time.seconds + self.sleep_time.microseconds / 1.0e6) self.max_restarts = int(section.max_restarts) self.start = as_boolean(section.start) self._stop = False self.status = 0
def process(self, args): """See `ICLISubCommand`.""" if args.output is None: output = sys.stdout else: # We don't need to close output because that will happen # automatically when the script exits. output = open(args.output, 'w') print(MAILMAN_VERSION_FULL, file=output) print('Python', sys.version, file=output) print('config file:', config.filename, file=output) print('db url:', config.db.url, file=output) print('devmode:', 'ENABLED' if as_boolean(config.devmode.enabled) else 'DISABLED', file=output) print('REST root url:', path_to('/', config.webservice.api_version), file=output) print('REST credentials: {0}:{1}'.format(config.webservice.admin_user, config.webservice.admin_pass), file=output) if args.verbose: print('File system paths:', file=output) longest = 0 paths = {} for attribute in dir(config): if attribute.endswith('_DIR') or attribute.endswith('_FILE'): paths[attribute] = getattr(config, attribute) longest = max(longest, len(attribute)) for attribute in sorted(paths): print(' {0:{2}} = {1}'.format(attribute, paths[attribute], longest))
def etag(resource): """Calculate the etag and return a JSON representation. The input is a dictionary representing the resource. This dictionary must not contain an `http_etag` key. This function calculates the etag by using the sha1 hexdigest of the pretty-printed (and thus key-sorted and predictable) representation of the dictionary. It then inserts this value under the `http_etag` key, and returns the JSON representation of the modified dictionary. :param resource: The original resource representation. :type resource: dictionary :return: JSON representation of the modified dictionary. :rtype string """ assert 'http_etag' not in resource, 'Resource already etagged' # Calculate the tag from a predictable (i.e. sorted) representation of the # dictionary. The actual details aren't so important. pformat() is # guaranteed to sort the keys, however it returns a str and the hash # library requires a bytes. Use the safest possible encoding. hashfood = pformat(resource).encode('raw-unicode-escape') etag = hashlib.sha1(hashfood).hexdigest() resource['http_etag'] = '"{}"'.format(etag) return json.dumps(resource, cls=ExtendedEncoder, sort_keys=as_boolean(config.devmode.enabled))
def sendmail(self, envsender, recipients, msgtext): """Mimic `smtplib.SMTP.sendmail`.""" if as_boolean(config.devmode.enabled): # Force the recipients to the specified address, but still deliver # to the same number of recipients. recipients = [config.devmode.recipient] * len(recipients) if self._connection is None: self._connect() # smtplib.SMTP.sendmail requires the message string to be pure ascii. # We have seen malformed messages with non-ascii unicodes, so ensure # we have pure ascii. msgtext = msgtext.encode('ascii', 'replace').decode('ascii') try: log.debug('envsender: %s, recipients: %s, size(msgtext): %s', envsender, recipients, len(msgtext)) results = self._connection.sendmail(envsender, recipients, msgtext) except smtplib.SMTPException: # For safety, close this connection. The next send attempt will # automatically re-open it. Pass the exception on up. self.quit() raise # This session has been successfully completed. self._session_count -= 1 # By testing exactly for equality to 0, we automatically handle the # case for SMTP_MAX_SESSIONS_PER_CONNECTION <= 0 meaning never close # the connection. We won't worry about wraparound <wink>. if self._session_count == 0: self.quit() return results
def dispose(mlist, msg, msgdata, why): if mlist.filter_action is FilterAction.reject: # Bounce the message to the original author. raise RejectMessage(why) elif mlist.filter_action is FilterAction.forward: # Forward it on to the list moderators. text = _("""\ The attached message matched the $mlist.display_name mailing list's content filtering rules and was prevented from being forwarded on to the list membership. You are receiving the only remaining copy of the discarded message. """) subject = _('Content filter message notification') notice = OwnerNotification(mlist, subject, roster=mlist.moderators) notice.set_type('multipart/mixed') notice.attach(MIMEText(text)) notice.attach(MIMEMessage(msg)) notice.send(mlist) # Let this fall through so the original message gets discarded. elif mlist.filter_action is FilterAction.preserve: if as_boolean(config.mailman.filtered_messages_are_preservable): # This is just like discarding the message except that a copy is # placed in the 'bad' queue should the site administrator want to # inspect the message. filebase = config.switchboards['bad'].enqueue(msg, msgdata) log.info('{} preserved in file base {}'.format( msg.get('message-id', 'n/a'), filebase)) else: log.error('{} invalid FilterAction: {}. Treating as discard'.format( mlist.fqdn_listname, mlist.filter_action.name)) # Most cases also discard the message raise DiscardMessage(why)
def put(self, mlist, attribute, value): # attribute will contain the (bytes) name of the archiver that is # getting a new status. value will be the representation of the new # boolean status. archiver = self._archiver_set.get(attribute) assert archiver is not None, attribute archiver.is_enabled = as_boolean(value)
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 put(self, mlist, attribute, value): # attribute will contain the (bytes) name of the archiver that is # getting a new status. value will be the representation of the new # boolean status. archiver = self._archiver_set.get(attribute) assert archiver is not None, attribute archiver.is_enabled = as_boolean(value)
def sendmail(self, envsender, recipients, msgtext): """Mimic `smtplib.SMTP.sendmail`.""" if as_boolean(config.devmode.enabled): # Force the recipients to the specified address, but still deliver # to the same number of recipients. recipients = [config.devmode.recipient] * len(recipients) if self._connection is None: self._connect() try: log.debug('envsender: %s, recipients: %s, size(msgtext): %s', envsender, recipients, len(msgtext)) results = self._connection.sendmail(envsender, recipients, msgtext) except smtplib.SMTPException: # For safety, close this connection. The next send attempt will # automatically re-open it. Pass the exception on up. self.quit() raise # This session has been successfully completed. self._session_count -= 1 # By testing exactly for equality to 0, we automatically handle the # case for SMTP_MAX_SESSIONS_PER_CONNECTION <= 0 meaning never close # the connection. We won't worry about wraparound <wink>. if self._session_count == 0: self.quit() return results
def is_testing(): """Return a 'testing' flag for use with the predictable factories. :return: True when in testing mode. :rtype: bool """ return MockAndMonkeyLayer.testing_mode or as_boolean(config.devmode.testing)
def initialize(self, debug=None): """See `IDatabase`.""" # Calculate the engine url. url = expand(config.database.url, config.paths) log.debug('Database url: %s', url) # XXX By design of SQLite, database file creation does not honor # umask. See their ticket #1193: # http://www.sqlite.org/cvstrac/tktview?tn=1193,31 # # This sucks for us because the mailman.db file /must/ be group # writable, however even though we guarantee our umask is 002 here, it # still gets created without the necessary g+w permission, due to # SQLite's policy. This should only affect SQLite engines because its # the only one that creates a little file on the local file system. # This kludges around their bug by "touch"ing the database file before # SQLite has any chance to create it, thus honoring the umask and # ensuring the right permissions. We only try to do this for SQLite # engines, and yes, we could have chmod'd the file after the fact, but # half dozen and all... self.url = url self._prepare(url) database = create_database(url) store = Store(database, GenerationalCache()) database.DEBUG = (as_boolean(config.database.debug) if debug is None else debug) self.store = store store.commit()
def dispose(mlist, msg, msgdata, why): if mlist.filter_action is FilterAction.reject: # Bounce the message to the original author. raise errors.RejectMessage(why) elif mlist.filter_action is FilterAction.forward: # Forward it on to the list moderators. text=_("""\ The attached message matched the $mlist.display_name mailing list's content filtering rules and was prevented from being forwarded on to the list membership. You are receiving the only remaining copy of the discarded message. """) subject=_('Content filter message notification') notice = OwnerNotification(mlist, subject, roster=mlist.moderators) notice.set_type('multipart/mixed') notice.attach(MIMEText(text)) notice.attach(MIMEMessage(msg)) notice.send(mlist) # Let this fall through so the original message gets discarded. elif mlist.filter_action is FilterAction.preserve: if as_boolean(config.mailman.filtered_messages_are_preservable): # This is just like discarding the message except that a copy is # placed in the 'bad' queue should the site administrator want to # inspect the message. filebase = config.switchboards['bad'].enqueue(msg, msgdata) log.info('{0} preserved in file base {1}'.format( msg.get('message-id', 'n/a'), filebase)) else: log.error( '{1} invalid FilterAction: {0}. Treating as discard'.format( mlist.fqdn_listname, mlist.filter_action.name)) # Most cases also discard the message raise errors.DiscardMessage(why)
def process(self, args): """See `ICLISubCommand`.""" if args.output is None: output = sys.stdout else: # We don't need to close output because that will happen # automatically when the script exits. output = open(args.output, 'w') print(MAILMAN_VERSION_FULL, file=output) print('Python', sys.version, file=output) print('config file:', config.filename, file=output) print('db url:', config.db.url, file=output) print('devmode:', 'ENABLED' if as_boolean(config.devmode.enabled) else 'DISABLED', file=output) api = (API30 if config.webservice.api_version == '3.0' else API31) print('REST root url:', api.path_to('/'), file=output) print('REST credentials: {}:{}'.format( config.webservice.admin_user, config.webservice.admin_pass), file=output) if args.verbose: print('File system paths:', file=output) longest = 0 paths = {} for attribute in dir(config): if attribute.endswith('_DIR') or attribute.endswith('_FILE'): paths[attribute] = getattr(config, attribute) longest = max(longest, len(attribute)) for attribute in sorted(paths): print(' {0:{2}} = {1}'.format( attribute, paths[attribute], longest))
def __init__(self, name, slice=None): """Create a runner. :param slice: The slice number for this runner. This is passed directly to the underlying `ISwitchboard` object. This is ignored for runners that don't manage a queue. :type slice: int or None """ # Grab the configuration section. self.name = name section = getattr(config, 'runner.' + name) substitutions = config.paths substitutions['name'] = name numslices = int(section.instances) # Check whether the runner is queue runner or not; non-queue runner # should not have queue_directory or switchboard instance. if self.is_queue_runner: self.queue_directory = expand(section.path, None, substitutions) self.switchboard = Switchboard(name, self.queue_directory, slice, numslices, True) else: self.queue_directory = None self.switchboard = None self.sleep_time = as_timedelta(section.sleep_time) # sleep_time is a timedelta; turn it into a float for time.sleep(). self.sleep_float = (86400 * self.sleep_time.days + self.sleep_time.seconds + self.sleep_time.microseconds / 1.0e6) self.max_restarts = int(section.max_restarts) self.start = as_boolean(section.start) self._stop = False self.status = 0
def put(self, mlist, attribute, value): # attribute will contain the (bytes) name of the archiver that is # getting a new status. value will be the representation of the new # boolean status. archiver = self._archiver_set.get(attribute) if archiver is None: raise ValueError('No such archiver: {}'.format(attribute)) archiver.is_enabled = as_boolean(value)
def is_testing(): """Return a 'testing' flag for use with the predictable factories. :return: True when in testing mode. :rtype: bool """ return (MockAndMonkeyLayer.testing_mode or as_boolean(config.devmode.testing))
def put(self, mlist, attribute, value): # attribute will contain the (bytes) name of the archiver that is # getting a new status. value will be the representation of the new # boolean status. archiver = self._archiver_set.get(attribute.decode('utf-8')) if archiver is None: raise ValueError('No such archiver: {}'.format(attribute)) archiver.is_enabled = as_boolean(value)
def path_to(cls, resource): """See `IAPI`.""" return '{}://{}:{}/{}/{}'.format( ('https' if as_boolean(config.webservice.use_https) else 'http'), config.webservice.hostname, config.webservice.port, cls.version, (resource[1:] if resource.startswith('/') else resource), )
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 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 path_to(cls, resource): """See `IAPI`.""" return '{}://{}:{}/{}/{}'.format( ('https' if as_boolean(config.webservice.use_https) else 'http'), config.webservice.hostname, config.webservice.port, cls.version, (resource[1:] if resource.startswith('/') else resource), )
def send_welcome_message(mlist, address, language, delivery_mode, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: the mailing list :type mlist: IMailingList :param address: The address to respond to :type address: string :param language: the language of the response :type language: ILanguage :param delivery_mode: the type of delivery the subscriber is getting :type delivery_mode: DeliveryMode """ if mlist.welcome_message_uri: try: uri = expand(mlist.welcome_message_uri, dict( listname=mlist.fqdn_listname, language=language.code, )) welcome_message = getUtility(ITemplateLoader).get(uri) except URLError: log.exception('Welcome message URI not found ({0}): {1}'.format( mlist.fqdn_listname, mlist.welcome_message_uri)) welcome = '' else: welcome = wrap(welcome_message) else: welcome = '' # Find the IMember object which is subscribed to the mailing list, because # from there, we can get the member's options url. member = mlist.members.get_member(address) user_name = member.user.display_name options_url = member.options_url # Get the text from the template. text = expand(welcome, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=user_name, user_address=address, user_options_uri=options_url, )) if delivery_mode is not DeliveryMode.regular: digmode = _(' (Digest mode)') else: digmode = '' msg = UserNotification( formataddr((user_name, address)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = _get_message(mlist.welcome_message_uri, mlist, language) options_url = member.options_url # Try to find a non-empty display name. We first look at the directly # subscribed record, which will either be the address or the user. That's # handled automatically by going through member.subscriber. If that # doesn't give us something useful, try whatever user is linked to the # subscriber. if member.subscriber.display_name: display_name = member.subscriber.display_name # If an unlinked address is subscribed tehre will be no .user. elif member.user is not None and member.user.display_name: display_name = member.user.display_name else: display_name = '' # Get the text from the template. text = expand( welcome_message, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=display_name, user_address=member.address.email, user_options_uri=options_url, )) digmode = ( '' # noqa if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr( (display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def _resource_as_dict(self, plugin_config): """See `CollectionMixin`.""" name, plugin_section = plugin_config resource = { 'name': name, 'class': plugin_section['class'], 'enabled': as_boolean(plugin_section['enabled']), } # Add the path to the plugin's own configuration file, if one was # given. plugin_config = plugin_section['configuration'].strip() if len(plugin_config) > 0: resource['configuration'] = plugin_config return resource
def check(self, mlist, msg, msgdata): """See `IRule`.""" if not as_boolean(config.mailman.hold_digest): return False # Convert the header value to a str because it may be an # email.header.Header instance. subject = str(msg.get('subject', '')).strip() if DIGRE.search(subject): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message has a digest subject')) return True # Get the masthead, but without emails. mastheadtxt = getUtility(ITemplateLoader).get( 'list:member:digest:masthead', mlist) mastheadtxt = wrap( expand( mastheadtxt, mlist, dict( display_name=mlist.display_name, listname='', list_id=mlist.list_id, request_email='', owner_email='', ))) msgtext = '' for part in msg.walk(): if part.get_content_maintype() == 'text': cset = part.get_content_charset('utf-8') msgtext += part.get_payload(decode=True).decode( cset, errors='replace') matches = 0 lines = mastheadtxt.splitlines() for line in lines: line = line.strip() if not line: continue if msgtext.find(line) >= 0: matches += 1 if matches >= int(config.mailman.masthead_threshold): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message quotes digest boilerplate')) return True return False
def path_to(resource): """Return the url path to a resource. :param resource: The canonical path to the resource, relative to the system base URI. :type resource: string :return: The full path to the resource. :rtype: bytes """ return b'{0}://{1}:{2}/{3}/{4}'.format( ('https' if as_boolean(config.webservice.use_https) else 'http'), config.webservice.hostname, config.webservice.port, config.webservice.api_version, (resource[1:] if resource.startswith('/') else resource), )
def path_to(resource): """Return the url path to a resource. :param resource: The canonical path to the resource, relative to the system base URI. :type resource: string :return: The full path to the resource. :rtype: bytes """ return '{0}://{1}:{2}/{3}/{4}'.format( ('https' if as_boolean(config.webservice.use_https) else 'http'), config.webservice.hostname, config.webservice.port, config.webservice.api_version, (resource[1:] if resource.startswith('/') else resource), )
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = _get_message(mlist.welcome_message_uri, mlist, language) options_url = member.options_url # Try to find a non-empty display name. We first look at the directly # subscribed record, which will either be the address or the user. That's # handled automatically by going through member.subscriber. If that # doesn't give us something useful, try whatever user is linked to the # subscriber. if member.subscriber.display_name: display_name = member.subscriber.display_name # If an unlinked address is subscribed tehre will be no .user. elif member.user is not None and member.user.display_name: display_name = member.user.display_name else: display_name = '' # Get the text from the template. text = expand(welcome_message, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=display_name, user_address=member.address.email, user_options_uri=options_url, )) digmode = ('' # noqa if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr((display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def initialize(propagate=None): """Initialize all logs. :param propagate: Flag specifying whether logs should propagate their messages to the root logger. If omitted, propagation is determined from the configuration files. :type propagate: bool or None """ # First, find the root logger and configure the logging subsystem. # Initialize the root logger, then create a formatter for all the # sublogs. The root logger should log to stderr. logging.basicConfig(format=config.logging.root.format, datefmt=config.logging.root.datefmt, level=as_log_level(config.logging.root.level), stream=sys.stderr) # Create the sub-loggers. Note that we'll redirect flufl.lock to # mailman.locks. for logger_config in config.logger_configs: sub_name = logger_config.name.split('.')[-1] if sub_name == 'root': continue if sub_name == 'locks': log = logging.getLogger('flufl.lock') else: logger_name = 'mailman.' + sub_name log = logging.getLogger(logger_name) # Get settings from log configuration file (or defaults). log_format = logger_config.format log_datefmt = logger_config.datefmt # Propagation to the root logger is how we handle logging to stderr # when the runners are not run as a subprocess of 'bin/mailman start'. log.propagate = (as_boolean(logger_config.propagate) if propagate is None else propagate) # Set the logger's level. log.setLevel(as_log_level(logger_config.level)) # Create a formatter for this logger, then a handler, and link the # formatter to the handler. formatter = logging.Formatter(fmt=log_format, datefmt=log_datefmt) path_str = logger_config.path path_abs = os.path.normpath(os.path.join(config.LOG_DIR, path_str)) handler = ReopenableFileHandler(sub_name, path_abs) _handlers[sub_name] = handler handler.setFormatter(formatter) log.addHandler(handler)
def _init_logger(propagate, sub_name, log, logger_config): # Get settings from log configuration file (or defaults). log_format = logger_config.format log_datefmt = logger_config.datefmt # Propagation to the root logger is how we handle logging to stderr # when the runners are not run as a subprocess of 'bin/mailman start'. log.propagate = (as_boolean(logger_config.propagate) if propagate is None else propagate) # Set the logger's level. log.setLevel(as_log_level(logger_config.level)) # Create a formatter for this logger, then a handler, and link the # formatter to the handler. formatter = logging.Formatter(fmt=log_format, datefmt=log_datefmt) path_str = logger_config.path path_abs = os.path.normpath(os.path.join(config.LOG_DIR, path_str)) handler = ReopenableFileHandler(sub_name, path_abs) _handlers[sub_name] = handler handler.setFormatter(formatter) log.addHandler(handler)
def _init_logger(propagate, sub_name, log, logger_config): # Get settings from log configuration file (or defaults). log_format = logger_config.format log_datefmt = logger_config.datefmt # Propagation to the root logger is how we handle logging to stderr when # the runners are not run as a subprocess of 'mailman start'. log.propagate = (as_boolean(logger_config.propagate) if propagate is None else propagate) # Set the logger's level. log.setLevel(as_log_level(logger_config.level)) # Create a formatter for this logger, then a handler, and link the # formatter to the handler. formatter = logging.Formatter(fmt=log_format, datefmt=log_datefmt) path_str = logger_config.path path_abs = os.path.normpath(os.path.join(config.LOG_DIR, path_str)) handler = ReopenableFileHandler(sub_name, path_abs) _handlers[sub_name] = handler handler.setFormatter(formatter) log.addHandler(handler)
def send_goodbye_message(mlist, address, language): """Send a goodbye message to a subscriber. Prepending to the standard goodbye message template is the mailing list's goodbye message, if there is one. :param mlist: the mailing list :type mlist: IMailingList :param address: The address to respond to :type address: string :param language: the language of the response :type language: string """ goodbye_message = _get_message(mlist.goodbye_message_uri, mlist, language) msg = UserNotification( address, mlist.bounces_address, _('You have been unsubscribed from the $mlist.display_name ' 'mailing list'), goodbye_message, language) msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = wrap( getUtility(ITemplateLoader).get('list:user:notice:welcome', mlist, language=language.code)) display_name = member.display_name # Get the text from the template. text = expand( welcome_message, mlist, dict( user_name=display_name, user_email=member.address.email, # For backward compatibility. user_address=member.address.email, fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, list_requests=mlist.request_address, )) digmode = ( '' # noqa: F841 if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr( (display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
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 send_goodbye_message(mlist, address, language): """Send a goodbye message to a subscriber. Prepending to the standard goodbye message template is the mailing list's goodbye message, if there is one. :param mlist: the mailing list :type mlist: IMailingList :param address: The address to respond to :type address: string :param language: the language of the response :type language: string """ goodbye_message = _get_message(mlist.goodbye_message_uri, mlist, language) msg = UserNotification( address, mlist.bounces_address, _('You have been unsubscribed from the $mlist.display_name ' 'mailing list'), goodbye_message, language) msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def send_user_disable_warning(mlist, address, language): """Sends a warning mail to the user reminding the person to reenable its DeliveryStatus. :param mlist: The mailing list :type mlist: IMailingList :param address: The address of the member :type address: string. :param language: member's preferred language :type language: ILanguage """ warning_message = wrap( getUtility(ITemplateLoader).get('list:user:notice:warning', mlist, language=language.code)) warning_message_text = expand(warning_message, mlist, dict(sender_email=address)) msg = UserNotification( address, mlist.bounces_address, _('Your subscription for ${mlist.display_name} mailing list' ' has been disabled'), warning_message_text, language) msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def __init__(self, name, slice=None): """Create a runner. :param slice: The slice number for this runner. This is passed directly to the underlying `ISwitchboard` object. This is ignored for runners that don't manage a queue. :type slice: int or None """ # Grab the configuration section. self.name = name section = getattr(config, "runner." + name) substitutions = config.paths substitutions["name"] = name self.queue_directory = expand(section.path, substitutions) numslices = int(section.instances) self.switchboard = Switchboard(name, self.queue_directory, slice, numslices, True) self.sleep_time = as_timedelta(section.sleep_time) # sleep_time is a timedelta; turn it into a float for time.sleep(). self.sleep_float = 86400 * self.sleep_time.days + self.sleep_time.seconds + self.sleep_time.microseconds / 1.0e6 self.max_restarts = int(section.max_restarts) self.start = as_boolean(section.start) self._stop = False
def start_runners(self, runner_names=None): """Start all the configured runners. :param runners: If given, a sequence of runner names to start. If not given, this sequence is taken from the configuration file. :type runners: a sequence of strings """ if not runner_names: runner_names = [] for runner_config in config.runner_configs: # Strip off the 'runner.' prefix. assert runner_config.name.startswith('runner.'), ( 'Unexpected runner configuration section name: {}'.format( runner_config.name)) runner_names.append(runner_config.name[7:]) # For each runner we want to start, find their config section, which # will tell us the name of the class to instantiate, along with the # number of hash space slices to manage. for name in runner_names: section_name = 'runner.' + name # Let AttributeError propagate. runner_config = getattr(config, section_name) if not as_boolean(runner_config.start): continue # Find out how many runners to instantiate. This must be a power # of 2. count = int(runner_config.instances) assert (count & (count - 1)) == 0, ( 'Runner "{0}", not a power of 2: {1}'.format(name, count)) for slice_number in range(count): # runner name, slice #, # of slices, restart count info = (name, slice_number, count, 0) spec = '{0}:{1:d}:{2:d}'.format(name, slice_number, count) pid = self._start_runner(spec) log = logging.getLogger('mailman.runner') log.debug('[{0:d}] {1}'.format(pid, spec)) self._kids.add(pid, info)
def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. Prepending to the standard welcome message template is the mailing list's welcome message, if there is one. :param mlist: The mailing list. :type mlist: IMailingList :param member: The member to send the welcome message to. :param address: IMember :param language: The language of the response. :type language: ILanguage """ welcome_message = _get_message(mlist.welcome_message_uri, mlist, language) options_url = member.options_url # Get the text from the template. display_name = ('' if member.user is None else member.user.display_name) text = expand( welcome_message, dict( fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, user_name=display_name, user_address=member.address.email, user_options_uri=options_url, )) digmode = ('' if member.delivery_mode is DeliveryMode.regular else _(' (Digest mode)')) msg = UserNotification( formataddr( (display_name, member.address.email)), mlist.request_address, _('Welcome to the "$mlist.display_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries))
def start_runners(self, runner_names=None): """Start all the configured runners. :param runners: If given, a sequence of runner names to start. If not given, this sequence is taken from the configuration file. :type runners: a sequence of strings """ if not runner_names: runner_names = [] for runner_config in config.runner_configs: # Strip off the 'runner.' prefix. assert runner_config.name.startswith('runner.'), ( 'Unexpected runner configuration section name: {0}'.format( runner_config.name)) runner_names.append(runner_config.name[7:]) # For each runner we want to start, find their config section, which # will tell us the name of the class to instantiate, along with the # number of hash space slices to manage. for name in runner_names: section_name = 'runner.' + name # Let AttributeError propagate. runner_config = getattr(config, section_name) if not as_boolean(runner_config.start): continue # Find out how many runners to instantiate. This must be a power # of 2. count = int(runner_config.instances) assert (count & (count - 1)) == 0, ( 'Runner "{0}", not a power of 2: {1}'.format(name, count)) for slice_number in range(count): # runner name, slice #, # of slices, restart count info = (name, slice_number, count, 0) spec = '{0}:{1:d}:{2:d}'.format(name, slice_number, count) pid = self._start_runner(spec) log = logging.getLogger('mailman.runner') log.debug('[{0:d}] {1}'.format(pid, spec)) self._kids.add(pid, info)
def process_event(self, store, event): """See `IBounceProcessor`.""" list_manager = getUtility(IListManager) mlist = list_manager.get(event.list_id) if mlist is None: # List was removed before the bounce is processed. event.processed = True # This needs an explicit commit because of the raise. config.db.commit() raise InvalidBounceEvent( 'Bounce for non-existent list {}'.format(event.list_id)) member = mlist.members.get_member(event.email) if member is None: event.processed = True # This needs an explicit commit because of the raise. config.db.commit() raise InvalidBounceEvent( 'Email {} is not a subcriber of {}'.format( event.email, mlist.list_id)) # If this is a probe bounce, that we are sent before to check for this # Mailbox, we just disable the delivery for this member. if event.context == BounceContext.probe: log.info('Probe bounce received for member %s on list %s.', event.email, mlist.list_id) event.processed = True self._disable_delivery(mlist, member, event) return # Looks like this is a regular bounce event, we need to process it # in the follow order: # 0. If the member is already disabled by bounce, we ignore this # event. # 1. If the date of the bounce is same as the previous bounce, we # ignore this event. # 2. Check if the bounce info is valid, bump the bounce_score and # update the last bounce info. # 3. If the bounce info is stale, reset the bounce score with # this new value. # 4. If the bounce_score is greater than threshold after the above, # a) Send a VERP probe, if configured to do so # b) Disable membership otherwise and notify the user and # warnings. if member.preferences.delivery_status == DeliveryStatus.by_bounces: log.info('Residual bounce received for member %s on list %s.', event.email, mlist.list_id) event.processed = True return if (member.last_bounce_received is not None and member.last_bounce_received.date() == event.timestamp.date()): # Update the timestamp of the last bounce received. member.last_bounce_received = event.timestamp event.processed = True log.info('Member %s already scored a bounce on list %s today.', event.email, mlist.list_id) return if member.last_bounce_received is None or ( member.last_bounce_received < event.timestamp - mlist.bounce_info_stale_after): # Reset the bounce score to 1, for the current bounce that we got. member.bounce_score = 1 else: # Update the bounce score to reflect this bounce. member.bounce_score += 1 # Update the last received time for the bounce. member.last_bounce_received = event.timestamp log.info('Member %s on list %s, bounce score = %d.', event.email, mlist.list_id, member.bounce_score) # Now, we are done updating. Let's see if the threshold is reached and # disable based on that. if member.bounce_score >= mlist.bounce_score_threshold: # Save bounce_score because sending probe resets it. saved_bounce_score = member.bounce_score if as_boolean(config.mta.verp_probes): send_probe(member, message_id=event.message_id) action = 'sending probe' else: self._disable_delivery(mlist, member, event) action = 'disabling delivery' log.info( 'Member %s on list %s, bounce score %d >= threshold %d, %s.', event.email, mlist.list_id, saved_bounce_score, mlist.bounce_score_threshold, action) event.processed = True
def process(self, mlist, msg, msgdata): """See `IHandler`.""" if as_boolean(config.mta.remove_dkim_headers): del msg['domainkey-signature'] del msg['dkim-signature'] del msg['authentication-results']
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)
def _dispose(self, mlist, msg, msgdata): # See if we should retry delivery of this message again. deliver_after = msgdata.get('deliver_after', datetime.fromtimestamp(0)) if now() < deliver_after: return True # Calculate whether we should VERP this message or not. The results of # this set the 'verp' key in the message metadata. interval = int(config.mta.verp_delivery_interval) if 'verp' in msgdata: # Honor existing settings. pass # If personalization is enabled for this list and we've configured # Mailman to always VERP personalized deliveries, then yes we VERP it. # Also, if personalization is /not/ enabled, but # verp_delivery_interval is set (and we've hit this interval), then # again, this message should be VERP'd. Otherwise, no. elif mlist.personalize != Personalization.none: if as_boolean(config.mta.verp_personalized_deliveries): msgdata['verp'] = True elif interval == 0: # Never VERP. msgdata['verp'] = False elif interval == 1: # VERP every time. msgdata['verp'] = True else: # VERP every 'interval' number of times. msgdata['verp'] = (mlist.post_id % interval == 0) try: debug_log.debug('[outgoing] {0}: {1}'.format( self._func, msg.get('message-id', 'n/a'))) self._func(mlist, msg, msgdata) self._logged = False except socket.error: # There was a problem connecting to the SMTP server. Log this # once, but crank up our sleep time so we don't fill the error # log. port = int(config.mta.smtp_port) if port == 0: port = 'smtp' # Log this just once. if not self._logged: log.error('Cannot connect to SMTP server %s on port %s', config.mta.smtp_host, port) self._logged = True return True except SomeRecipientsFailed as error: processor = getUtility(IBounceProcessor) # BAW: msg is the original message that failed delivery, not a # bounce message. This may be confusing if this is what's sent to # the user in the probe message. Maybe we should craft a # bounce-like message containing information about the permanent # SMTP failure? if 'probe_token' in msgdata: # This is a failure of our local MTA to deliver to a probe # message recipient. Register the bounce event for permanent # failures. Start by grabbing and confirming (i.e. removing) # the pendable record associated with this bounce token, # regardless of what address was actually failing. if len(error.permanent_failures) > 0: pended = getUtility(IPendings).confirm( msgdata['probe_token']) # It's possible the token has been confirmed out of the # database. Just ignore that. if pended is not None: # The UUID had to be pended as a unicode. member = getUtility(ISubscriptionService).get_member( UUID(hex=pended['member_id'])) processor.register( mlist, member.address.email, msg, BounceContext.probe) else: # Delivery failed at SMTP time for some or all of the # recipients. Permanent failures are registered as bounces, # but temporary failures are retried for later. for email in error.permanent_failures: processor.register(mlist, email, msg, BounceContext.normal) # Move temporary failures to the qfiles/retry queue which will # occasionally move them back here for another shot at # delivery. if error.temporary_failures: current_time = now() recipients = error.temporary_failures last_recip_count = msgdata.get('last_recip_count', 0) deliver_until = msgdata.get('deliver_until', current_time) if len(recipients) == last_recip_count: # We didn't make any progress. If we've exceeded the # configured retry period, log this failure and # discard the message. if current_time > deliver_until: smtp_log.error('Discarding message with ' 'persistent temporary failures: ' '{0}'.format(msg['message-id'])) return False else: # We made some progress, so keep trying to delivery # this message for a while longer. deliver_until = current_time + as_timedelta( config.mta.delivery_retry_period) msgdata['last_recip_count'] = len(recipients) msgdata['deliver_until'] = deliver_until msgdata['recipients'] = recipients self._retryq.enqueue(msg, msgdata) # We've successfully completed handling of this message. return False
def process(self, mlist, msg, msgdata): """See `IHandler`.""" if as_boolean(config.mta.remove_dkim_headers): del msg['domainkey-signature'] del msg['dkim-signature'] del msg['authentication-results']
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 _dispose(self, mlist, msg, msgdata): # See if we should retry delivery of this message again. deliver_after = msgdata.get('deliver_after', datetime.fromtimestamp(0)) if now() < deliver_after: return True # Calculate whether we should VERP this message or not. The results of # this set the 'verp' key in the message metadata. interval = int(config.mta.verp_delivery_interval) if 'verp' in msgdata: # Honor existing settings. pass # If personalization is enabled for this list and we've configured # Mailman to always VERP personalized deliveries, then yes we VERP it. # Also, if personalization is /not/ enabled, but # verp_delivery_interval is set (and we've hit this interval), then # again, this message should be VERP'd. Otherwise, no. elif mlist.personalize != Personalization.none: if as_boolean(config.mta.verp_personalized_deliveries): msgdata['verp'] = True elif interval == 0: # Never VERP. msgdata['verp'] = False elif interval == 1: # VERP every time. msgdata['verp'] = True else: # VERP every 'interval' number of times. msgdata['verp'] = (mlist.post_id % interval == 0) try: debug_log.debug('[outgoing] {}: {}'.format( self._func, msg.get('message-id', 'n/a'))) self._func(mlist, msg, msgdata) self._logged = False except socket.error: # There was a problem connecting to the SMTP server. Log this # once, but crank up our sleep time so we don't fill the error # log. port = int(config.mta.smtp_port) if port == 0: port = 'smtp' # Log this just once. if not self._logged: log.error('Cannot connect to SMTP server %s on port %s', config.mta.smtp_host, port) self._logged = True return True except SomeRecipientsFailed as error: processor = getUtility(IBounceProcessor) # BAW: msg is the original message that failed delivery, not a # bounce message. This may be confusing if this is what's sent to # the user in the probe message. Maybe we should craft a # bounce-like message containing information about the permanent # SMTP failure? if 'probe_token' in msgdata: # This is a failure of our local MTA to deliver to a probe # message recipient. Register the bounce event for permanent # failures. Start by grabbing and confirming (i.e. removing) # the pendable record associated with this bounce token, # regardless of what address was actually failing. if len(error.permanent_failures) > 0: pended = getUtility(IPendings).confirm( msgdata['probe_token']) # It's possible the token has been confirmed out of the # database. Just ignore that. if pended is not None: # The UUID had to be pended as a unicode. member = getUtility(ISubscriptionService).get_member( UUID(hex=pended['member_id'])) processor.register(mlist, member.address.email, msg, BounceContext.probe) else: # Delivery failed at SMTP time for some or all of the # recipients. Permanent failures are registered as bounces, # but temporary failures are retried for later. for email in error.permanent_failures: processor.register(mlist, email, msg, BounceContext.normal) # Move temporary failures to the qfiles/retry queue which will # occasionally move them back here for another shot at # delivery. if error.temporary_failures: current_time = now() recipients = error.temporary_failures last_recip_count = msgdata.get('last_recip_count', 0) deliver_until = msgdata.get('deliver_until', current_time) if len(recipients) == last_recip_count: # We didn't make any progress. If we've exceeded the # configured retry period, log this failure and # discard the message. if current_time > deliver_until: smtp_log.error('Discarding message with ' 'persistent temporary failures: ' '{}'.format(msg['message-id'])) return False else: # We made some progress, so keep trying to delivery # this message for a while longer. deliver_until = current_time + as_timedelta( config.mta.delivery_retry_period) msgdata['last_recip_count'] = len(recipients) msgdata['deliver_until'] = deliver_until msgdata['recipients'] = recipients self._retryq.enqueue(msg, msgdata) # We've successfully completed handling of this message. return False
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)
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)